Changeset 19438


Ignore:
Timestamp:
02/05/19 16:08:39 (6 years ago)
Author:
tbretz
Message:
This is a new version which is supposed to be able schedule ToOs
File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/FACT++/src/scheduler.cc

    r19125 r19438  
    1 #include <vector>
    2 
    3 #include <boost/regex.hpp>
    4 
    5 #include <mysql++/mysql++.h>
     1#include "Prediction.h"
     2
     3#include <boost/algorithm/string/join.hpp>
     4
     5#include "Database.h"
     6
     7#include "HeadersToO.h"
     8#include "HeadersGCN.h"
     9
     10#include "EventImp.h"
     11#include "LocalControl.h"
     12#include "StateMachineDim.h"
    613
    714#include "Dim.h"
     15#include "tools.h"
    816#include "Time.h"
    9 #include "Event.h"
    10 #include "Connection.h"
    11 #include "LocalControl.h"
    1217#include "Configuration.h"
    13 #include "StateMachineDim.h"
    14 
    15 #include "tools.h"
    1618
    1719using namespace std;
    18 using namespace boost::gregorian;
    19 using namespace boost::posix_time;
    20 
    21 // things to be done/checked/changed
    22 // * --schedule-database should be required
    23 // * move definition of config parameters to AutoScheduler class
    24 //   + read in from config
    25 // * in some (all?) loops iterator over vector can be replaced by counter
    26 
    27 // other things to do
    28 //
    29 // define what to transmit as info/warn/error
    30 
    31 
    32 // config parameters:
    33 //   mintime
    34 //   runtimec
    35 //   runtimep
    36 //   repostime
    37 
    38 // missing:
    39 //
    40 // calculate time for std obs
    41 // calculate sun set/rise
    42 //
    43 // return errors and other otherput from sendcommand to webinterface
    44 
    45 // in which cases should the scheduler go in error state?
    46 //   when db is unavailable
    47 // does one also need a 'set scheduler to ready' function then?
    48 // do we want any error state at all?
    49 
    50 
    51 // =========================================================================
    52 
    53 template <class T>
    54 class AutoScheduler : public T
     20using namespace Nova;
     21
     22// -----------------------------------------------------------------------
     23
     24/*
     25void SetupConfiguration(Configuration &conf);
    5526{
    56     bool fNextIsPreview;
    57 public:
    58     enum states_t
    59     {
    60         kSM_Scheduling=1,
    61         kSM_Comitting,
    62     };
    63 
    64     struct ObservationParameters
    65     {
    66         int obskey;
    67         int obsmode;
    68         int obstype;
    69         int splitflag;
    70         int telsetup;
    71         float fluxweight;
    72         float slope;
    73         float flux;
    74         float ra;
    75         float dec;
    76         ptime start;
    77         ptime stop;
    78         time_duration duration_db;
    79         string sourcename;
    80         int sourcekey;
    81     };
    82 
    83     struct FixedObs
    84     {
    85         int obskey;
    86         int sourcekey;
    87         string sourcename;
    88         int obsmode;
    89         int obstype;
    90         int telsetup;
    91         float ra;
    92         float dec;
    93         ptime start;
    94         ptime stop;
    95     };
    96 
    97     // will need other types of obs
    98     // FloatingObs (duration < stop-start + splitflag no)
    99     // FloatingSplittedObs (duration < stop-start + splitflag yes)
    100     // FixedSlot, i.e. just block a time slot
    101 
    102     struct StdObs
    103     {
    104         int obskey_std;
    105         int sourcekey_std;
    106         string sourcename_std;
    107         int obsmode_std;
    108         int obstype_std;
    109         int telsetup_std;
    110         float fluxweight_std;
    111         float slope_std;
    112         float flux_std;
    113         float ra_std;
    114         float dec_std;
    115         ptime obsstdstart;
    116         ptime obsstdstop;
    117     };
    118 
    119     struct ScheduledObs
    120     {
    121         int obskey_obs;
    122         int sourcekey_obs;
    123         string sourcename_obs;
    124         int obsmode_obs;
    125         int obstype_obs;
    126         int telsetup_obs;
    127         ptime obsstart;
    128         ptime obsstop;
    129     };
    130 
    131     struct ScheduledRun
    132     {
    133         //int runnumber; // to be seen, if runnumber is needed
    134         int obskey_run;
    135         int runtype;
    136         int sourcekey_run;
    137         string sourcename_run;//for convenience
    138         int obsmode_run;
    139         int obstype_run;
    140         int telsetup_run;
    141         ptime runstart;
    142         ptime runstop;
    143     };
    144 
    145     string fDatabase;
    146     string fDBName;
    147     int fDurationCalRun; //unit: minutes
    148     int fDurationPedRun; //unit: minutes
    149     int fDurationRepos; //unit: minutes
    150 
    151     int Schedule()
    152     {
    153         bool error = false;
    154 
    155         time_duration runtimec(0, fDurationCalRun, 0);
    156         time_duration runtimep(0, fDurationPedRun, 0);
    157         time_duration repostime(0, fDurationRepos, 0);
    158         time_duration mintime(1, 0, 0);
    159 
    160         const ptime startsched(microsec_clock::local_time());
    161         const ptime stopsched=startsched+years(1);
    162 
    163         ostringstream str;
    164         str << "Scheduling for the period from " << startsched << " to " << stopsched;
    165         T::Message(str);
    166 
    167         static const boost::regex expr("([[:word:].-]+):(.+)@([[:word:].-]+)(:([[:digit:]]+))?/([[:word:].-]+)");
    168         // 2: user
    169         // 4: pass
    170         // 5: server
    171         // 7: port
    172         // 9: db
    173 
    174         boost::smatch what;
    175         if (!boost::regex_match(fDatabase, what, expr, boost::match_extra))
    176         {
    177             ostringstream msg;
    178             msg << "Regex to parse database '" << fDatabase << "' empty.";
    179             T::Error(msg);
    180             return T::kSM_Error;
    181         }
    182 
    183         if (what.size()!=7)
    184         {
    185             ostringstream msg;
    186             msg << "Parsing database name failed: '" << fDatabase << "'";
    187             T::Error(msg);
    188             return T::kSM_Error;
    189         }
    190 
    191         const string user   = what[1];
    192         const string passwd = what[2];
    193         const string server = what[3];
    194         const string db     = fDBName.empty() ? what[6] : fDBName;
    195         const int    port   = stoi(what[5]);
    196 
    197         ostringstream dbnamemsg;
    198         dbnamemsg << "Scheduling started -> using database " << db << ".";
    199         T::Message(dbnamemsg);
    200 
    201         str.str("");
    202         str << "Connecting to '";
    203         if (!user.empty())
    204             str << user << "@";
    205         str << server;
    206         if (port)
    207             str << ":" << port;
    208         if (!db.empty())
    209             str << "/" << db;
    210         str << "'";
    211         T::Info(str);
    212 
    213         mysqlpp::Connection conn(db.c_str(), server.c_str(), user.c_str(), passwd.c_str(), port);
    214         /* throws exceptions
    215         if (!conn.connected())
    216         {
    217             ostringstream msg;
    218             msg << "MySQL connection error: " << conn.error();
    219             T::Error(msg);
    220             return T::kSM_Error;
    221         }*/
    222 
    223         // get observation parameters from DB
    224         // maybe order by priority?
    225         const mysqlpp::StoreQueryResult res =
    226             conn.query("SELECT fObservationKEY, fStartTime, fStopTime, fDuration, fSourceName, fSourceKEY, fSplitFlag, fFluxWeight, fSlope, fFlux, fRightAscension, fDeclination, fObservationModeKEY, fObservationTypeKEY , fTelescopeSetupKEY FROM ObservationParameters LEFT JOIN Source USING(fSourceKEY) ORDER BY fStartTime").store();
    227         // FIXME: Maybe we have to check for a successfull
    228         //        query but an empty result
    229         /* thorws exceptions?
    230         if (!res)
    231         {
    232             ostringstream msg;
    233             msg << "MySQL query failed: " << query.error();
    234             T::Error(msg);
    235             return T::kSM_Error;
    236         }*/
    237 
    238         str.str("");
    239         str << "Found " << res.num_rows() << " Observation Parameter sets.";
    240         T::Debug(str);
    241 
    242         ObservationParameters olist[res.num_rows()];
    243         vector<FixedObs>     obsfixedlist;
    244         vector<StdObs>       obsstdlist;
    245         vector<ScheduledObs> obslist;
    246         vector<ScheduledRun> runlist;
    247 
    248         // loop over observation parameters from DB
    249         // fill these parameters into FixedObs and StdObs
    250         int counter=0;
    251         int counter2=0;
    252         int counter3=0;
    253         cout << "Obs: <obskey> <sourcename>(<sourcekey>, <fluxweight>) from <starttime> to <stoptime>" << endl;
    254         for (vector<mysqlpp::Row>::const_iterator v=res.begin(); v<res.end(); v++)
    255         {
    256             cout << "  Obs: " << (*v)[0].c_str() << " " << (*v)[4].c_str() << "(" << (*v)[5].c_str() << flush;
    257             cout << ", " << (*v)[7].c_str() << ")" << flush;
    258             cout << " from " << (*v)[1].c_str() << " to " << (*v)[2].c_str() << endl;
    259 
    260             //0: obskey
    261             //1: startime
    262             //2: stoptime
    263             //3: duration
    264             //4: sourcename
    265             //5: sourcekey
    266             //6: splitflag
    267             //7: fluxweight
    268             //8: slope
    269             //9: flux
    270             //10: ra
    271             //11: dec
    272             //12: obsmode
    273             //13: obstype
    274             //14: telsetup
    275             stringstream t1;
    276             stringstream t2;
    277             stringstream t3;
    278             t1 << (*v)[1].c_str();
    279             t2 << (*v)[2].c_str();
    280             t3 << (*v)[3].c_str();
    281 
    282             //boost::posix_time::time_duration mintime(0,conf.Get<int>("mintime"), 0);
    283             t1 >> Time::sql >> olist[counter].start;
    284             t2 >> Time::sql >> olist[counter].stop;
    285             t3 >> olist[counter].duration_db;
    286             const time_period period(olist[counter].start, olist[counter].stop);
    287 
    288             olist[counter].sourcename=(*v)[4].c_str();
    289             olist[counter].sourcekey=(*v)[5];
    290 
    291             if (!(*v)[0].is_null())
    292                 olist[counter].obskey=(*v)[0];
    293             if (!(*v)[12].is_null())
    294                 olist[counter].obsmode=(*v)[12];
    295             if (!(*v)[13].is_null())
    296                 olist[counter].obstype=(*v)[13];
    297             if (!(*v)[14].is_null())
    298                 olist[counter].telsetup=(*v)[14];
    299             if (!(*v)[6].is_null())
    300                 olist[counter].splitflag=(*v)[6];
    301             if (!(*v)[7].is_null())
    302                 olist[counter].fluxweight=(*v)[7];
     27    po::options_description control("Makeschedule");
     28    control.add_options()
     29        ("uri", var<string>(), "Database link as in\n\tuser:password@server[:port]/database[?compress=0|1].")
     30        ;
     31
     32    // po::positional_options_description p;
     33    // p.add("date", 1); // The first positional options
     34
     35    conf.AddOptions(control);
     36    //conf.SetArgumentPositions(p);
     37}
     38
     39void PrintUsage();
     40{
     41    cout <<
     42        "makeschedule - Creates an automatic schedule\n"
     43        "\n"
     44        "Usage: makeschedule [yyyy-mm-dd]\n";
     45    cout << endl;
     46}
     47
     48void PrintHelp();
     49{
     50    cout << endl;
     51}*/
     52
     53struct CheckVisibility
     54{
     55    // Input
     56    float moon_min     =  10;
     57    float moon_max     = 170;
     58    float sun_max      = -12;
     59    float zd_max       =  75;
     60    float current_max  = 110;
     61
     62    // Output
     63    Nova::SolarObjects solarobj;
     64    Nova::ZdAzPosn position;
     65
     66    double moon_dist   = -1;
     67    double current     = -1;
     68
     69    bool valid_zd      = false;
     70    bool valid_current = false;
     71    bool valid_sun     = false;
     72    bool valid_moon    = false;
     73
     74    bool visible       = false;
     75
     76    void calc(const Nova::EquPosn &equ, const double &jd)
     77    {
     78        solarobj  = Nova::SolarObjects(jd);
     79        position  = Nova::GetHrzFromEqu(equ, jd);
     80        moon_dist = Nova::GetAngularSeparation(equ, solarobj.fMoonEqu);
     81
     82        current   = FACT::PredictI(solarobj, equ);
     83
     84        valid_moon    = moon_dist>moon_min && moon_dist<moon_max;
     85        valid_zd      = position.zd<zd_max;
     86        valid_sun     = solarobj.fSunHrz.alt<sun_max;
     87        valid_current = current<current_max;
     88
     89        visible = valid_moon && valid_zd && valid_sun && valid_current;
     90    }
     91
     92    CheckVisibility(const Nova::EquPosn &equ, const double &jd)
     93    {
     94        calc(equ, jd);
     95    }
     96
     97    CheckVisibility(const Nova::EquPosn &equ)
     98    {
     99        calc(equ, Time().JD());
     100    }
     101
     102    CheckVisibility(const double &ra, const double &dec, const double &jd)
     103    {
     104        Nova::EquPosn equ;
     105        equ.ra  = ra;
     106        equ.dec = dec;
     107        calc(equ, jd);
     108    }
     109
     110    CheckVisibility(const double &ra, const double &dec)
     111    {
     112        Nova::EquPosn equ;
     113        equ.ra  = ra;
     114        equ.dec = dec;
     115        calc(equ, Time().JD());
     116    }
     117};
     118
     119/*
     120
     121 * Visibility check
     122 * Source eintragen
     123 * RELOAD SOURCES
     124 * Schedule eintragen
     125 * Send 'RESCHEDULE' interrupt
     126
     127*/
     128/*
     129int mainx(int argc, const char* argv[])
     130{
     131    Configuration conf(argv[0]);
     132    conf.SetPrintUsage(PrintUsage);
     133    SetupConfiguration(conf);
     134
     135    if (!conf.DoParse(argc, argv, PrintHelp))
     136        return 127;
     137
     138    // ------------------ Eval config ---------------------
     139
     140    const string uri = conf.Get<string>("schedule-database");
     141
     142    const uint16_t verbose = 1;
     143    const bool dry_run = true;
     144
     145    Time stopwatch;
     146
     147    Database connection(uri); // Keep alive while fetching rows
     148
     149    cout << Time()-stopwatch << endl;
     150
     151    if (verbose>0)
     152        cout << "Server Version: " << connection.server_version() << endl;
     153
     154    const int source_key = 999;      // source_key of the source to be scheduled.
     155
     156    const uint16_t contingency =   0; // The contingency to delete earlier entries from the Table
     157    const uint16_t duration    = 300; // The target duration of the ToO
     158    const uint16_t required    =  20; // If the ToO would be less than required, skip_it
     159    const uint16_t skip_time   =  20; // If following observation would be less than skip_time, skip it
     160
     161    if (duration<required)
     162    {
     163        cerr << "Requested duration is smaller than required time." << endl;
     164        return 0;
     165    }
     166
     167    Time day(2019, 1, 24, 17,  0,  0);
     168    Time now(2019, 1, 24, 19, 59,  0);
     169    Time next    = day+boost::posix_time::hours(14);
     170    Time restart = now+boost::posix_time::minutes(duration);
     171
     172    // Safety margin
     173    now -= boost::posix_time::seconds(contingency);
     174
     175    cout << '\n';
     176    cout << "Contingency:   " << contingency << " s\n";
     177    cout << "Start of ToO:  " << now.GetAsStr() << '\n';
     178    cout << "End   of ToO:  " << restart.GetAsStr() << '\n';
     179    cout << "Duration:      " << duration << "  min\n\n";
     180
     181    cout << "Schedule:\n";
     182
     183    const string queryS =
     184        "SELECT *,"
     185        " (fMeasurementTypeKey=4 AND fStart<='"+restart.GetAsStr()+"') AS `Reschedule`,"
     186        " (fStart>'"+restart.GetAsStr()+"') AS `Following`,"
     187        " (fMeasurementTypeKey=4 AND fStart BETWEEN '"+now.GetAsStr()+"' AND '"+restart.GetAsStr()+"') AS `Delete`"
     188        " FROM Schedule"
     189        " WHERE fStart BETWEEN '"+day.GetAsStr()+"' AND '"+next.GetAsStr()+"'"
     190        " AND fMeasurementID=0"
     191        " ORDER BY fStart ASC, fMeasurementID ASC";
     192
     193    const mysqlpp::StoreQueryResult resS =
     194        connection.query(queryS).store();
     195
     196    vector<string> list;
     197
     198    int32_t firstdata  = -1;
     199    int32_t reschedule = -1;
     200    int32_t following  = -1;
     201    for (size_t i=0; i<resS.num_rows(); i++)
     202    {
     203        const mysqlpp::Row &row = resS[i];
     204
     205        cout << setw(2) << i << " | ";
     206        cout << row["Reschedule"]     << " | ";
     207        cout << row["Following"]      << " | ";
     208        cout << row["fScheduleID"]    << " | ";
     209        cout << row["fStart"]         << " | ";
     210        if (bool(row["Delete"]))
     211        {
     212            cout << "     < delete >     | ";
     213            list.push_back((string)row["fScheduleID"]);
     214        }
     215        else
     216            cout << row["fLastUpdate"]    << " | ";
     217        cout << row["fMeasurementID"] << " | ";
     218        cout << row["fUser"]          << " | ";
     219        cout << row["fData"]          << " | " << setw(4);
     220        cout << row["fSourceKey"]     << " | ";
     221        cout << row["fMeasurementTypeKey"] << '\n';
     222
     223        if (bool(row["Reschedule"]))
     224            reschedule = i;
     225
     226        if (following==-1 && bool(row["Following"]))
     227            following = i;
     228
     229        uint16_t type = row["fMeasurementTypeKey"];
     230        if (firstdata==-1 && type==4)
     231            firstdata = i;
     232    }
     233
     234    // --------------------------------------------------------------------
     235
     236    if (firstdata==-1)
     237    {
     238        cerr << "No data run found." << endl;
     239        return 0;
     240    }
     241
     242    // --------------------------------------------------------------------
     243
     244    if (reschedule>=0)
     245        cout << "\nLast data run before restart at " << restart.GetAsStr() << ": idx=" << reschedule << " / ID=" << resS[reschedule]["fScheduleID"] << "\n";
     246
     247    // --------------------------------------------------------------------
     248
     249    const Time first((string)resS[firstdata]["fStart"]);          // First data run of the night
     250    const Time last( (string)resS[resS.num_rows()-1]["fStart"]);  // Last scheduled observation of the night
     251
     252    // --------------------------------------------------------------------
     253
     254    if (restart<=first)
     255    {
     256        cout << "ToO ends before first data taking... skipped." << endl;
     257        return 0;
     258    }
     259    if (now>=last)
     260    {
     261        cout << "ToO starts after data taking... skipped." << endl;
     262        return 0;
     263    }
     264
     265    // --------------------------------------------------------------------
     266
     267    if (now<first)
     268    {
     269        cout << "ToO starts before first data taking... rescheduling start time." << endl;
     270        now = first;
     271    }
     272    if (restart>last)
     273    {
     274        cout << "ToO ends after data taking... rescheduling end time!" << endl;
     275        restart = last;
     276    }
     277
     278    // --------------------------------------------------------------------
     279
     280    if (restart<now+boost::posix_time::minutes(required))
     281    {
     282        cout << "Can not schedule more than " << required << "minutes... skipped." << endl;
     283        return 0;
     284    }
     285
     286    // --------------------------------------------------------------------
     287
     288    if (following>=0)
     289    {
     290        const Time follow((string)resS[following]["fStart"]);
     291        if (follow<restart+boost::posix_time::minutes(skip_time))
     292        {
     293            cout << "Following observation would be less than " << skip_time << " min... skipping rescheduled source." << endl;
     294            reschedule = -1;
     295        }
     296    }
     297
     298    // ====================================================================
     299
     300    if (!list.empty())
     301    {
     302        cout << "\nDelete entries:\n";
     303        for (const auto &l : list)
     304            cout << l << "\n";
     305        cout << endl;
     306    }
     307
     308    // ====================================================================
     309
     310    vector<string> insert;
     311
     312    cout << "New entries:\n";
     313    cout << "       | auto  | " << now.GetAsStr()     << " |       < start >     | 0 | ToO  | NULL |      | 4\n";
     314
     315    insert.emplace_back("('"+now.GetAsStr()+"',0,'ToO',NULL,"+to_string(source_key)+",4)");
     316
     317    // --------------------------------------------------------------------
     318
     319    if (reschedule>=0 && restart!=last)
     320    {
     321        cout << "       | auto  | " << restart.GetAsStr() << " |        < end >      | 0 | ToO  | NULL | " << setw(4) << resS[reschedule]["fSourceKey"] << " | 4\n";
     322
     323        const string fData = (string)resS[reschedule]["fData"];
     324        const string fKey  = (string)resS[reschedule]["fSourceKey"];
     325
     326        const string data  = resS[reschedule]["fData"] ? "'"+fData+"'" : "NULL";
     327        insert.emplace_back("('"+restart.GetAsStr()+"',0,'ToO',"+data+","+fKey+",4)");
     328    }
     329
     330    // ====================================================================
     331
     332    cout << Time()-stopwatch << endl;
     333
     334    // ====================================================================
     335
     336    if (insert.empty())
     337    {
     338        cout << "No new schedule." << endl;
     339        return 0;
     340    }
     341
     342    const string queryD = string("DELETE FROM Schedule WHERE fScheduleID IN (")+boost::algorithm::join(list, ",")+")";
     343
     344    const string queryI =
     345        "INSERT (fStart, fMeasurementID, fUser, fData, fSoureKey, fMeasurementTypeKey) "
     346        "VALUES\n "+string(boost::algorithm::join(insert, ",\n "));
     347
     348    if (dry_run)
     349    {
     350        cout << "\n";
     351        if (!list.empty())
     352            cout << queryD << "\n\n";
     353        if (!insert.empty())
     354            cout << queryI << "\n\n";
     355        cout << flush;
     356        return 0;
     357    }
     358
     359    // Empty interrupt to stop data taking as early as possible
     360    Dim::SendCommandNB("DIM_CONTROL/INTERRUPT");
     361
     362    // Reload sources as early as possible
     363    Dim::SendCommandNB("DRIVE_CONTROL/RELOAD_SOURCES");
     364
     365    // Start pointing procedure as early as possible
     366    //Dim::SendCommand("DRIVE_CONTROL/TRACK_WOBBLE", wobble, obs[sub].source);
     367
     368    try
     369    {
     370        // ---------------------------------------------------------
     371
     372        connection.query("LOCK TABLES Schedule WRITE");
     373
     374        // ---------------------------------------------------------
     375
     376        if (!list.empty())
     377        {
     378            const mysqlpp::SimpleResult res = connection.query(queryD).execute();
     379            cout << res.rows() << " row(s) deleted.\n" << endl;
     380        }
     381
     382        // ---------------------------------------------------------
     383
     384        if (!insert.empty())
     385        {
     386            auto q = connection.query(queryI);
     387            q.execute();
     388            cout << q.info() << '\n' << endl;
     389        }
     390
     391        // ---------------------------------------------------------
     392
     393        connection.query("UNLOCK TABLES");
     394
     395        // ---------------------------------------------------------
     396    }
     397    catch (const exception &e)
     398    {
     399        cerr << "SQL query failed: " << e.what() << endl;
     400        return 7;
     401    }
     402
     403    Dim::SendCommand("DIM_CONTROL/INTERRUPT", "reschedule");
     404
     405    return 0;
     406}
     407*/
     408// ------------------------------------------------------------------------
     409
     410class StateMachineToO : public StateMachineDim
     411{
     412private:
     413    string   fUri;
     414    Database fConnection;
     415    uint16_t fVerbose;
     416    bool     fDryRun;
     417
     418    Time     fKeepAliveDeadline;
     419    uint16_t fKeepAliveInterval;
     420
     421    bool ScheduleImp(const string &name, const double &ra, const double &dec)
     422    {
     423        fDryRun = true;
     424
     425        Time stopwatch;
     426
     427        // This is just a desperate try which will most likely fail
     428        if (!fConnection.connected())
     429            fConnection.reconnect();
     430
     431        /*
     432         +-----------------+---------------------+------+-----+---------+----------------+
     433         | Field           | Type                | Null | Key | Default | Extra          |
     434         +-----------------+---------------------+------+-----+---------+----------------+
     435         | fSourceKEY      | int(11)             | NO   | PRI | NULL    | auto_increment |
     436         | fSourceName     | varchar(30)         | NO   |     | NULL    |                |
     437         | fRightAscension | double(10,6)        | NO   |     | NULL    |                |
     438         | fDeclination    | double(10,6)        | NO   |     | NULL    |                |
     439         | fEpochKEY       | tinyint(4)          | YES  |     | NULL    |                |
     440         | fFlux           | float               | YES  |     | NULL    |                |
     441         | fSlope          | float               | YES  |     | NULL    |                |
     442         | fSourceTypeKey  | int(11)             | NO   | MUL | NULL    |                |
     443         | fWobbleOffset   | float               | NO   |     | 0.6     |                |
     444         | fWobbleAngle0   | int(11)             | NO   |     | 90      |                |
     445         | fWobbleAngle1   | int(11)             | NO   |     | -90     |                |
     446         | fMagnitude      | float(4,2)          | YES  |     | NULL    |                |
     447         | fIsToO          | tinyint(3) unsigned | NO   |     | 0       |                |
     448         +-----------------+---------------------+------+-----+---------+----------------+
     449         */
     450
     451        const double min_dist = 0.1;
     452        const double wobble_offset = 0.6;
     453        const double camera_radius = 2.3;
     454        const double magnitude_max = 4.5;
     455
     456        double wobble_angle  = 0;
     457
     458        /*
     459         Mag Cnt
     460         -2  1
     461         -1  3
     462          0  11
     463          1  33
     464          2  121
     465          3  321
     466          4  871
     467          5  1364
     468          6  404
     469          7  12
     470        */
     471
     472        // The wobble position lay in the plane whihc is normal the to the plane source-center-star
     473        // and goes through
     474
     475        const string query0 =
     476            "SELECT fSourceKEY, fRightAscension, fDeclination, fMagnitude,\n"
     477            " ADIST(fDeclination, fRightAscension*15, "+to_string(dec)+", "+to_string(ra)+") AS Dist\n"
     478            " FROM Source\n"
     479            " WHERE (fSourceTypeKey=2 OR fSourceTypeKey=3) AND fMagnitude<"+to_string(magnitude_max)+"\n"
     480            " HAVING Dist<"+to_string(camera_radius+wobble_offset)+"\n"
     481            " ORDER BY fMagnitude ASC";
     482
     483        Out() << query0 << endl;
     484
     485        const mysqlpp::StoreQueryResult res0 = fConnection.query(query0).store();
     486
     487        Out() << Time()-stopwatch << endl;
     488
     489        Info("Found "+to_string(res0.num_rows())+" stars in the camera field-of-view with magnitude less than "+to_string(magnitude_max));
     490
     491        for (size_t i=0; i<::min<size_t>(10, res0.num_rows()); i++)
     492        {
     493            const mysqlpp::Row &row = res0[i];
     494
     495            // TVector3 souce, star;
     496            // source.SetMagThetaPhi(1, rad(90-dec), rad(ra));
     497            // star.SetMagThetaPhi(  1, rad(90-dec), rad(ra));
     498            //
     499            // star.RotateZ(  -source.Phi());
     500            // source.RotateZ(-source.Phi());
     501            //
     502            // star.RotateY(  180-source.Theta());
     503            // source.RotateY(180-source.Theta());
     504            //
     505            // rho = star.Phi();
     506
     507            const double rad = M_PI/180;
     508
     509            const double dec0 = rad * dec;
     510            const double ra0  = rad * ra;
     511
     512            const double dec1 = rad * row["fDeclination"];
     513            const double ra1  = rad * row["fRightAscension"] * 15;
     514
     515            const double s2 = sin(ra0-ra1);
     516            const double c2 = cos(ra0-ra1);
     517
     518            const double s0 = cos(dec0);
     519            const double c0 = sin(dec0);
     520
     521            const double c1 = cos(dec1);
     522            const double s1 = sin(dec1);
     523
     524            const double k0 =           -s2*c1;
     525            const double k1 = s0*s1 - c0*c2*c1;
     526
     527            const double rho = atan(k0/k1) / rad; // atan2(k0, k1)/rad
     528
     529            if (i==0)
     530                wobble_angle = rho;
     531
     532            Info(" "+Tools::Form("Mag=%5.2f  Dist=%5.1f  Phi=%6.1f", (double)row["fMagnitude"], (double)row["Dist"], rho));
     533        }
     534
     535        if (res0.num_rows())
     536        {
     537            Info("The brightest star (M="+string(res0[0]["fMagnitude"])+") at distance is visible at a wobble angle of "+Tools::Form("%.2f", wobble_angle)+"\u00b0");
     538            Info("Wobble angles determined as "+Tools::Form("%.2f", wobble_angle-90)+"\u00b0 and "+Tools::Form("%.2f", wobble_angle+90)+"\u000b");
     539        }
     540        else
     541        {
     542            Info("Using default wobble angles.");
     543        }
     544
     545        const string query1 =
     546            "SELECT fSourceKey, fSourceName\n"
     547            " FROM Source\n"
     548            " WHERE ADIST(fDeclination, fRightAscension*15, "+to_string(dec)+", "+to_string(ra)+")<"+to_string(min_dist)+"\n"
     549            " ORDER BY fSourceKey ASC";
     550
     551        const mysqlpp::StoreQueryResult res1 = fConnection.query(query1).store();
     552
     553        if (res1.num_rows())
     554        {
     555            Warn("The following sources in the source table have a distance less than "+to_string(min_dist)+"\u00b0");
     556
     557            for (size_t i=0; i<res1.num_rows(); i++)
     558            {
     559                const mysqlpp::Row &row = res1[i];
     560                Warn(" "+string(row["fSourceName"])+" ["+to_string(uint32_t(row["fSourceKey"]))+"]");
     561            }
     562        }
     563
     564        Out() << Time()-stopwatch << endl;
     565
     566        int32_t source_key = -1;
     567
     568        const string query2 =
     569            "SELECT fSourceKey FROM Source WHERE fSourceName='"+name+"'";
     570
     571        const mysqlpp::StoreQueryResult res2 = fConnection.query(query2).store();
     572
     573        if (res2.num_rows())
     574        {
     575            source_key = res2[0]["fSourceKey"];
     576            Info("A source with the same name (key="+to_string(source_key)+") was found in the Source table.");
     577        }
     578
     579        if (source_key<0)
     580        {
     581            const string query =
     582                "INSERT INTO Source\n"
     583                " (fSourceName, fRightAscension, fDeclination, fWobbleAngle0, fWobbleAngle1, fSourceTypeKey, fIsToO) VALUES\n"
     584                " ('"+name+"', "+to_string(ra/15.)+", "+to_string(dec)+", "+to_string(wobble_angle-90)+", "+to_string(wobble_angle+90)+", 1, 1)";
     585
     586            if (!fDryRun)
     587            {
     588                auto q = fConnection.query(query);
     589                q.execute();
     590                Info(q.info());
     591
     592                source_key = q.insert_id();
     593
     594                Info(string(fDryRun?"[dry-run] ":"")+"The new source got the key "+to_string(source_key));
     595            }
    303596            else
    304                 olist[counter].fluxweight=0;//set fluxweight to 0 for check below
    305             if (!(*v)[8].is_null())
    306                 olist[counter].slope=(*v)[8];
    307             if (!(*v)[9].is_null())
    308                 olist[counter].flux=(*v)[9];
    309             if (!(*v)[10].is_null())
    310                 olist[counter].ra=(*v)[10];
    311             if (!(*v)[11].is_null())
    312                 olist[counter].dec=(*v)[11];
    313 
    314             // time_duration cannot be used, as only up to 99 hours are handeled
    315             // const time_duration duration = period.length();
    316 
    317             /*
    318             if (olist[counter].stoptime < olist[counter].starttime+mintime)
    319                 cout << "  ====> WARN: Observation too short. " << endl;
    320 
    321             if (olist[counter].starttime.is_not_a_date_time())
    322                 cout << "  WARN: starttime not a date_time. " << endl;
     597                Out() << query << endl;
     598
     599        }
     600
     601        Out() << Time()-stopwatch << endl;
     602
     603        /*
     604         +---------------------+--------------+------+-----+-------------------+-----------------------------+
     605         | Field               | Type         | Null | Key | Default           | Extra                       |
     606         +---------------------+--------------+------+-----+-------------------+-----------------------------+
     607         | fScheduleID         | int(11)      | NO   | PRI | NULL              | auto_increment              |
     608         | fStart              | datetime     | NO   | MUL | NULL              |                             |
     609         | fLastUpdate         | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
     610         | fMeasurementID      | int(11)      | NO   |     | NULL              |                             |
     611         | fUser               | varchar(32)  | NO   |     | NULL              |                             |
     612         | fData               | varchar(256) | YES  |     | NULL              |                             |
     613         | fSourceKey          | smallint(6)  | YES  |     | NULL              |                             |
     614         | fMeasurementTypeKey | tinyint(4)   | NO   |     | NULL              |                             |
     615         +---------------------+--------------+------+-----+-------------------+-----------------------------+
     616         */
     617
     618        const uint16_t contingency =   1; // The contingency to delete earlier entries from the Table
     619        const uint16_t duration    =  60; // The target duration of the ToO
     620        const uint16_t required    =  20; // If the ToO would be less than required, skip_it
     621        const uint16_t skip_time   =  20; // If following observation would be less than skip_time, skip it
     622
     623        if (duration<required)
     624        {
     625            Error("Requested duration is smaller than required time.");
     626            return false;
     627        }
     628
     629        const Time now;
     630
     631        const Time sunset  = now.GetPrevSunSet();
     632        const Time sunrise = now.GetNextSunRise();
     633
     634        if (sunset>sunrise || sunrise>sunset+boost::posix_time::hours(24))
     635        {
     636            Error("Unable to schedule a ToO during daytime.");
     637            return false;
     638        }
     639
     640        // Safety margin
     641        Time schedtime = now-boost::posix_time::seconds(contingency);
     642        Time restart   = now+boost::posix_time::minutes(duration);
     643
     644        Out() << '\n';
     645        Out() << "Contingency:   " << contingency << " s\n";
     646        Out() << "Start of ToO:  " << now.GetAsStr() << '\n';
     647        Out() << "End   of ToO:  " << restart.GetAsStr() << '\n';
     648        Out() << "Duration:      " << duration << " min\n\n";
     649
     650        Out() << "Schedule:\n";
     651
     652        const string queryS =
     653            "SELECT *,\n"
     654            " (fMeasurementTypeKey=4 AND fStart<='"+restart.GetAsStr()+"') AS `Reschedule`,\n"
     655            " (fStart>'"+restart.GetAsStr()+"') AS `Following`,\n"
     656            " (fMeasurementTypeKey=4 AND fStart BETWEEN '"+schedtime.GetAsStr()+"' AND '"+restart.GetAsStr()+"') AS `Delete`\n"
     657            " FROM Schedule\n"
     658            " WHERE fStart BETWEEN '"+sunset.GetAsStr()+"' AND '"+sunrise.GetAsStr()+"'\n"
     659            " AND fMeasurementID=0\n"
     660            " ORDER BY fStart ASC, fMeasurementID ASC";
     661
     662        const mysqlpp::StoreQueryResult resS = fConnection.query(queryS).store();
     663
     664        if (resS.num_rows()<2)
     665        {
     666            Error("Available schedule is too short to be evaluated.");
     667            return false;
     668        }
     669
     670        vector<string> list;
     671
     672        int32_t firstdata  = -1;
     673        int32_t reschedule = -1;
     674        int32_t following  = -1;
     675        for (size_t i=0; i<resS.num_rows(); i++)
     676        {
     677            const mysqlpp::Row &row = resS[i];
     678
     679            Out() << setw(2) << i << " | ";
     680            Out() << row["Reschedule"]     << " | ";
     681            Out() << row["Following"]      << " | ";
     682            Out() << row["fScheduleID"]    << " | ";
     683            Out() << row["fStart"]         << " | ";
     684            if (bool(row["Delete"]))
     685            {
     686                Out() << "     < delete >     | ";
     687                list.push_back((string)row["fScheduleID"]);
     688            }
    323689            else
    324                 cout << "  start:   " << Time::sql << olist[counter].starttime << endl;
    325             if (olist[counter].stoptime.is_not_a_date_time())
    326                 cout << "  WARN: stoptime not a date_time. " << endl;
    327             else
    328                 cout << "  stop:   " << Time::sql << olist[counter].stoptime << endl;
    329             if (!(olist[counter].starttime.is_not_a_date_time() || olist[counter].stoptime.is_not_a_date_time()))
    330                 cout << "  diff:   " << period << endl;
    331             if (olist[counter].stoptime < olist[counter].starttime)
    332                 cout << "  ====> WARN: stop time (" << olist[counter].stoptime << ") < start time (" << olist[counter].starttime << "). " << endl;
    333             cout << "diff:   " << duration << flush;
    334             cout << "dur_db:   " << olist[counter].duration_db << endl;
    335             */
    336 
    337             // always filled: obstype
    338             //
    339             // fixed observations:
    340             //   filled: starttime, stoptime
    341             //   not filled: fluxweight
    342             //   maybe filled: obsmode, telsetup, source (not filled for FixedSlotObs)
    343             //   maybe filled: duration (filled for FloatingObs and FloatingSplittedObs)
    344             //   maybe filled: splitflag (filled for FloatingSplittedObs)
    345             //
    346             // std observations:
    347             //   filled: fluxweight, telsetup, obsmore, source
    348             //   not filled: starttime, stoptime, duration
    349 
    350             // fixed observations
    351             if (!(olist[counter].stop.is_not_a_date_time()
    352                   && olist[counter].start.is_not_a_date_time())
    353                 && olist[counter].fluxweight==0
    354                )
     690                Out() << row["fLastUpdate"]    << " | ";
     691            Out() << row["fMeasurementID"] << " | ";
     692            Out() << row["fUser"]          << " | ";
     693            Out() << row["fData"]          << " | " << setw(4);
     694            Out() << row["fSourceKey"]     << " | ";
     695            Out() << row["fMeasurementTypeKey"] << '\n';
     696
     697            if (bool(row["Reschedule"]))
     698                reschedule = i;
     699
     700            if (following==-1 && bool(row["Following"]))
     701                following = i;
     702
     703            const uint16_t type = row["fMeasurementTypeKey"];
     704            if (firstdata==-1 && type==4)
     705                firstdata = i;
     706        }
     707
     708        // --------------------------------------------------------------------
     709
     710        if (firstdata==-1)
     711        {
     712            Warn("No data run found.");
     713            return false;
     714        }
     715
     716        // --------------------------------------------------------------------
     717
     718        if (reschedule>=0)
     719            Out() << "\nLast data run before restart at " << restart.GetAsStr() << ": idx=" << reschedule << " / ID=" << resS[reschedule]["fScheduleID"] << endl;
     720
     721        // --------------------------------------------------------------------
     722
     723        const Time first((string)resS[firstdata]["fStart"]);          // First data run of the night
     724        const Time last( (string)resS[resS.num_rows()-1]["fStart"]);  // Last scheduled observation of the night
     725
     726        // --------------------------------------------------------------------
     727
     728        if (restart<=first)
     729        {
     730            Warn("ToO ends before first data taking... skipped.");
     731            return false;
     732        }
     733        if (schedtime>=last)
     734        {
     735            Warn("ToO starts after data taking... skipped.");
     736            return false;
     737        }
     738
     739        // --------------------------------------------------------------------
     740
     741        if (schedtime<first)
     742        {
     743            Info("ToO starts before first data taking... rescheduling start time.");
     744            schedtime = first;
     745        }
     746        if (restart>last)
     747        {
     748            Info("ToO ends after data taking... rescheduling end time!");
     749            restart = last;
     750        }
     751
     752        // --------------------------------------------------------------------
     753
     754        if (restart<schedtime+boost::posix_time::minutes(required))
     755        {
     756            Warn("Could not schedule more than the required "+to_string(required)+" minutes... skipped.");
     757            return false;
     758        }
     759
     760        // --------------------------------------------------------------------
     761
     762        if (following>=0)
     763        {
     764            const Time follow((string)resS[following]["fStart"]);
     765            if (follow<restart+boost::posix_time::minutes(skip_time))
    355766            {
    356                 obsfixedlist.resize(counter2+1);
    357                 obsfixedlist[counter2].start=olist[counter].start;
    358                 obsfixedlist[counter2].stop=olist[counter].stop;
    359                 obsfixedlist[counter2].sourcename=olist[counter].sourcename;
    360                 obsfixedlist[counter2].obskey=olist[counter].obskey;
    361                 obsfixedlist[counter2].obstype=olist[counter].obstype;
    362                 obsfixedlist[counter2].obsmode=olist[counter].obsmode;
    363                 obsfixedlist[counter2].telsetup=olist[counter].telsetup;
    364                 obsfixedlist[counter2].sourcekey=olist[counter].sourcekey;
    365                 obsfixedlist[counter2].ra=olist[counter].ra;
    366                 obsfixedlist[counter2].dec=olist[counter].dec;
    367                 counter2++;
     767                Info("Following observation would be less than "+to_string(skip_time)+" min... skipping rescheduled source.");
     768                reschedule = -1;
    368769            }
    369 
    370             // std obs
    371             if (olist[counter].stop.is_not_a_date_time()
    372                   && olist[counter].start.is_not_a_date_time()
    373                 && olist[counter].fluxweight>0
    374                )
    375             {
    376                 obsstdlist.resize(counter3+1);
    377                 obsstdlist[counter3].sourcename_std=olist[counter].sourcename;
    378                 obsstdlist[counter3].obskey_std=olist[counter].obskey;
    379                 obsstdlist[counter3].obsmode_std=olist[counter].obsmode;
    380                 obsstdlist[counter3].obstype_std=olist[counter].obstype;
    381                 obsstdlist[counter3].telsetup_std=olist[counter].telsetup;
    382                 obsstdlist[counter3].sourcekey_std=olist[counter].sourcekey;
    383                 obsstdlist[counter3].fluxweight_std=olist[counter].fluxweight;
    384                 obsstdlist[counter3].flux_std=olist[counter].flux;
    385                 obsstdlist[counter3].slope_std=olist[counter].slope;
    386                 obsstdlist[counter3].ra_std=olist[counter].ra;
    387                 obsstdlist[counter3].dec_std=olist[counter].dec;
    388                 counter3++;
    389             }
    390 
    391             counter++;
    392         }
    393         ostringstream fixedobsmsg;
    394         fixedobsmsg << obsfixedlist.size() << " fixed observations found. ";
    395         T::Message(fixedobsmsg);
    396         cout << obsfixedlist.size() << " fixed observations found. " << endl;
    397 
    398         ostringstream stdobsmsg;
    399         stdobsmsg << obsstdlist.size() << " standard observations found. ";
    400         T::Message(stdobsmsg);
    401         cout << obsstdlist.size() << " standard observations found. " << endl;
    402 
    403         // loop to add the fixed observations to the ScheduledObs list
    404         // performed checks:
    405         //   * overlap of fixed observations: the overlap is split half-half
    406         //   * check for scheduling time range: only take into account fixed obs within the range
    407         // missing checks and evaluation
    408         //   * check for mintime (pb with overlap checks)
    409         //   * check for sun
    410         //   * check for moon
    411         counter2=0;
    412         int skipcounter=0;
    413         ptime finalobsfixedstart;
    414         ptime finalobsfixedstop;
    415         time_duration delta0(0,0,0);
    416 
    417         cout << "Fixed Observations: " << endl;
    418         for (struct vector<FixedObs>::const_iterator vobs=obsfixedlist.begin(); vobs!=obsfixedlist.end(); vobs++)
    419         {
    420             if (obsfixedlist[counter2].start < startsched
    421                 || obsfixedlist[counter2].stop > stopsched)
    422             {
    423                 ostringstream skipfixedobsmsg;
    424                 skipfixedobsmsg << "Skip 1 fixed observation (obskey ";
    425                 skipfixedobsmsg << obsfixedlist[counter2].obskey;
    426                 skipfixedobsmsg << ") as it is out of scheduling time range.";
    427                 T::Message(skipfixedobsmsg);
    428 
    429                 counter2++;
    430                 skipcounter++;
    431                 continue;
    432             }
    433             counter3=0;
    434 
    435             time_duration delta1=delta0;
    436             time_duration delta2=delta0;
    437 
    438             finalobsfixedstart=obsfixedlist[counter2].start;
    439             finalobsfixedstop=obsfixedlist[counter2].stop;
    440 
    441             for (struct vector<FixedObs>::const_iterator vobs5=obsfixedlist.begin(); vobs5!=obsfixedlist.end(); vobs5++)
    442             {
    443                 if (vobs5->start < obsfixedlist[counter2].stop
    444                     && obsfixedlist[counter2].stop <= vobs5->stop
    445                     && obsfixedlist[counter2].start <= vobs5->start
    446                     && counter2!=counter3)
    447                 {
    448                     delta1=(obsfixedlist[counter2].stop-vobs5->start)/2;
    449                     finalobsfixedstop=obsfixedlist[counter2].stop-delta1;
    450 
    451                     ostringstream warndelta1;
    452                     warndelta1 << "Overlap between two fixed observations (";
    453                     warndelta1 << obsfixedlist[counter2].obskey << " ";
    454                     warndelta1 << vobs5->obskey << "). The stoptime of ";
    455                     warndelta1 << obsfixedlist[counter2].obskey << " has been changed.";
    456                     T::Warn(warndelta1);
    457                 }
    458                 if (vobs5->start <= obsfixedlist[counter2].start
    459                     && obsfixedlist[counter2].start < vobs5->stop
    460                     && obsfixedlist[counter2].stop >= vobs5->stop
    461                     && counter2!=counter3)
    462                 {
    463                     delta2=(vobs5->stop-obsfixedlist[counter2].start)/2;
    464                     finalobsfixedstart=obsfixedlist[counter2].start+delta2;
    465 
    466                     ostringstream warndelta2;
    467                     warndelta2 << "Overlap between two fixed observations (";
    468                     warndelta2 << obsfixedlist[counter2].obskey << " ";
    469                     warndelta2 << vobs5->obskey << "). The starttime of ";
    470                     warndelta2 << obsfixedlist[counter2].obskey << " has been changed.";
    471 
    472                     T::Warn(warndelta2);
    473                 }
    474                 counter3++;
    475             }
    476 
    477             const int num=counter2-skipcounter;
    478             obslist.resize(num+1);
    479             obslist[num].obsstart=finalobsfixedstart;
    480             obslist[num].obsstop=finalobsfixedstop;
    481             obslist[num].sourcename_obs=obsfixedlist[counter2].sourcename;
    482             obslist[num].obsmode_obs=obsfixedlist[counter2].obsmode;
    483             obslist[num].obstype_obs=obsfixedlist[counter2].obstype;
    484             obslist[num].telsetup_obs=obsfixedlist[counter2].telsetup;
    485             obslist[num].sourcekey_obs=obsfixedlist[counter2].sourcekey;
    486             obslist[num].obskey_obs=obsfixedlist[counter2].obskey;
    487             counter2++;
    488 
    489             cout << "  " << vobs->sourcename <<  " " << vobs->start;
    490             cout << " - " << vobs->stop << endl;
    491         }
    492         ostringstream obsmsg;
    493         obsmsg << "Added " << obslist.size() << " fixed observations to ScheduledObs. ";
    494         T::Message(obsmsg);
    495         cout << "Added " << obslist.size() << " fixed observations to ScheduledObs. " << endl;
    496 
    497         for (int i=0; i<(int)obsstdlist.size(); i++)
    498         {
    499             for (int j=0; j<(int)obsstdlist.size(); j++)
    500             {
    501                 if (obsstdlist[i].sourcekey_std == obsstdlist[j].sourcekey_std && i!=j)
    502                 {
    503                     cout << "One double sourcekey in std observations: " << obsstdlist[j].sourcekey_std << endl;
    504                     ostringstream errdoublestd;
    505                     errdoublestd << "One double sourcekey in std observations: " << obsstdlist[j].sourcekey_std << " (" << obsstdlist[j].sourcename_std << ").";
    506                     T::Error(errdoublestd);
    507                     T::Message("Scheduling stopped.");
    508                     return error ? T::kSM_Error : T::kSM_Ready;
    509                 }
    510             }
    511         }
    512 
    513         // loop over nights
    514         //   calculate sunset and sunrise
    515         //   check if there is already scheduled obs in that night
    516         //
    517 
    518         // in this loop the standard observations shall be
    519         // checked, evaluated
    520         // the observation times shall be calculated
    521         // and the observations added to the ScheduledObs list
    522         cout << "Standard Observations: " << endl;
    523         for (struct vector<StdObs>::const_iterator vobs2=obsstdlist.begin(); vobs2!=obsstdlist.end(); vobs2++)
    524         {
    525             cout << "  " << vobs2->sourcename_std << endl;
    526         }
    527 
    528         // in this loop the ScheduledRuns are filled
    529         //  (only data runs -> no runtype yet)
    530         // might be merged with next loop
    531         counter2=0;
    532         for (struct vector<ScheduledObs>::const_iterator vobs3=obslist.begin(); vobs3!=obslist.end(); vobs3++)
    533         {
    534             runlist.resize(counter2+1);
    535             runlist[counter2].runstart=obslist[counter2].obsstart;
    536             runlist[counter2].runstop=obslist[counter2].obsstop;
    537             runlist[counter2].sourcename_run=obslist[counter2].sourcename_obs;
    538             runlist[counter2].obsmode_run=obslist[counter2].obsmode_obs;
    539             runlist[counter2].obstype_run=obslist[counter2].obstype_obs;
    540             runlist[counter2].telsetup_run=obslist[counter2].telsetup_obs;
    541             runlist[counter2].sourcekey_run=obslist[counter2].sourcekey_obs;
    542             runlist[counter2].obskey_run=obslist[counter2].obskey_obs;
    543             counter2++;
    544             //cout << (*vobs3).sourcename_obs << endl;
    545         }
    546 
    547         //delete old scheduled runs from the DB
    548         const mysqlpp::SimpleResult res0 =
    549             conn.query("DELETE FROM ScheduledRun").execute();
    550         // FIXME: Maybe we have to check for a successfull
    551         //        query but an empty result
    552         /* throws exceptions
    553         if (!res0)
    554         {
    555             ostringstream msg;
    556             msg << "MySQL query failed: " << query0.error();
    557             T::Error(msg);
    558             return T::kSM_Error;
    559         }*/
    560 
    561         // in this loop the ScheduledRuns are inserted to the DB
    562         //   before the runtimes are adapted according to
    563         //   duration of P-Run, C-Run and repositioning
    564         counter3=0;
    565         int insertcount=0;
    566         ptime finalstarttime;
    567         ptime finalstoptime;
    568         for (struct vector<ScheduledRun>::const_iterator vobs4=runlist.begin(); vobs4!=runlist.end(); vobs4++)
    569         {
    570             for (int i=2; i<5; i++)
    571             {
    572                 switch(i)
    573                 {
    574                 case 2:
    575                     finalstarttime=runlist[counter3].runstart+repostime+runtimec+runtimep;
    576                     finalstoptime=runlist[counter3].runstop;
    577                     break;
    578                 case 3:
    579                     finalstarttime=runlist[counter3].runstart+repostime;
    580                     finalstoptime=runlist[counter3].runstart+runtimep+repostime;
    581                     break;
    582                 case 4:
    583                     finalstarttime=runlist[counter3].runstart+runtimep+repostime;
    584                     finalstoptime=runlist[counter3].runstart+repostime+runtimep+runtimec;
    585                     break;
    586                 }
    587                 ostringstream q1;
    588                 //cout << (*vobs4).sourcename_run << endl;
    589                 q1 << "INSERT ScheduledRun set fStartTime='" << Time::sql << finalstarttime;
    590                 q1 << "', fStopTime='" << Time::sql << finalstoptime;
    591                 q1 << "', fSourceKEY='" << (*vobs4).sourcekey_run;
    592                 q1 << "', fObservationKEY='" << (*vobs4).obskey_run;
    593                 q1 << "', fRunTypeKEY='" << i;
    594                 q1 << "', fTelescopeSetupKEY='" << (*vobs4).telsetup_run;
    595                 q1 << "', fObservationTypeKEY='" << (*vobs4).obstype_run;
    596                 q1 << "', fObservationModeKEY='" << (*vobs4).obsmode_run;
    597                 q1 << "'";
    598 
    599                 //cout << "executing query: " << q1.str() << endl;
    600 
    601                 const mysqlpp::SimpleResult res1 = conn.query(q1.str()).execute();
    602                 // FIXME: Maybe we have to check for a successfull
    603                 //        query but an empty result
    604                 /* throws exceptions
    605                 if (!res1)
    606                 {
    607                     ostringstream msg;
    608                     msg << "MySQL query failed: " << query1.error();
    609                     T::Error(str);
    610                     return T::kSM_Error;
    611                 }*/
    612                 insertcount++;
    613             }
    614             counter3++;
    615         }
    616         ostringstream insertmsg;
    617         insertmsg << "Inserted " << insertcount << " runs into the DB.";
    618         T::Message(insertmsg);
    619         //usleep(3000000);
    620         T::Message("Scheduling done.");
    621 
    622         return error;
    623     }
    624 
    625     /*
    626     // commit probably done by webinterface
    627     int Commit()
    628     {
    629         ostringstream str;
    630         str << "Comitting preview (id=" << fSessionId << ")";
    631         T::Message(str);
    632 
    633         usleep(3000000);
    634         T::Message("Comitted.");
    635 
    636         fSessionId = -1;
    637 
    638         bool error = false;
    639         return error ? T::kSM_Error : T::kSM_Ready;
    640     }
    641     */
    642 
    643     AutoScheduler(ostream &out=cout) : T(out, "SCHEDULER"), fNextIsPreview(true), fDBName("")
    644     {
    645         AddStateName(kSM_Scheduling, "Scheduling", "Scheduling in progress.");
    646 
    647         AddEvent(kSM_Scheduling, "SCHEDULE", "C", T::kSM_Ready)
    648             ("FIXME FIXME FIXME (explanation for the command)"
    649              "|database[string]:FIXME FIXME FIMXE (meaning and format)");
    650 
    651         AddEvent(T::kSM_Ready, "RESET", T::kSM_Error)
    652             ("Reset command to get out of the error state");
    653 
    654         //AddEvent(kSM_Comitting,  "COMMIT",   T::kSM_Ready);
    655 
    656         T::PrintListOfEvents();
     770        }
     771
     772        // ====================================================================
     773
     774        if (!list.empty())
     775        {
     776            Out() << "\nDelete entries:\n";
     777            for (const auto &l : list)
     778                Out() << l << "\n";
     779            Out() << endl;
     780        }
     781
     782        // ====================================================================
     783
     784        Info("Source will be scheduled.");
     785
     786        vector<string> insert;
     787
     788        Out() << "New entries:\n";
     789        Out() << "           | auto  | " << schedtime.GetAsStr()     << " |       < start >     | 0 | ToO  | nodrs,grb | " << setw(4) << source_key << " | 4\n";
     790
     791        insert.emplace_back("('"+schedtime.GetAsStr()+"',0,'ToO','nodrs:true,grb:true',"+to_string(source_key)+",4)");
     792
     793        // --------------------------------------------------------------------
     794
     795        if (reschedule>=0 && restart!=last)
     796        {
     797            Out() << "           | auto  | " << restart.GetAsStr() << " |        < end >      | 0 | ToO  |   NULL    | " << setw(4) << resS[reschedule]["fSourceKey"] << " | 4\n";
     798
     799            const string fData = (string)resS[reschedule]["fData"];
     800            const string fKey  = (string)resS[reschedule]["fSourceKey"];
     801
     802            const string data  = resS[reschedule]["fData"] ? "'"+fData+"'" : "NULL";
     803            insert.emplace_back("('"+restart.GetAsStr()+"',0,'ToO',"+data+","+fKey+",4)");
     804        }
     805
     806        // ====================================================================
     807
     808        Out() << Time()-stopwatch << endl;
     809
     810        // ====================================================================
     811
     812        const string queryD = string("DELETE FROM Schedule WHERE fScheduleID IN (")+boost::algorithm::join(list, ",")+")";
     813
     814        const string queryI =
     815            "INSERT INTO Schedule\n (fStart,fMeasurementID,fUser,fData,fSoureKey,fMeasurementTypeKey)\n"
     816            "VALUES\n "+string(boost::algorithm::join(insert, ",\n "));
     817
     818        if (fDryRun)
     819        {
     820            Out() << "\n";
     821            if (!list.empty())
     822                Out() << queryD << "\n\n";
     823            if (!insert.empty())
     824                Out() << queryI << "\n\n";
     825            Out() << flush;
     826            return false;
     827        }
     828
     829        // Empty interrupt to stop data taking as early as possible
     830        Dim::SendCommandNB("DIM_CONTROL/INTERRUPT");
     831
     832        // Reload sources as early as possible
     833        Dim::SendCommandNB("DRIVE_CONTROL/RELOAD_SOURCES");
     834
     835        // Start pointing procedure as early as possible
     836        //Dim::SendCommand("DRIVE_CONTROL/TRACK_WOBBLE", wobble, obs[sub].source);
     837
     838        // ---------------------------------------------------------
     839
     840        fConnection.query("LOCK TABLES Schedule WRITE");
     841
     842        // ---------------------------------------------------------
     843        /*
     844         if (!list.empty())
     845         {
     846         const mysqlpp::SimpleResult res = fConnection.query(queryD).execute();
     847         Info(to_string(res.rows())+" row(s) deleted from Schedule.");
     848         }
     849
     850         // ---------------------------------------------------------
     851
     852         if (!insert.empty())
     853         {
     854         auto q = fConnection.query(queryI);
     855         q.execute();
     856         Info(q.info());
     857         }
     858         */
     859        // ---------------------------------------------------------
     860
     861        fConnection.query("UNLOCK TABLES");
     862
     863        // ---------------------------------------------------------
     864
     865        Dim::SendCommand("DIM_CONTROL/INTERRUPT", "reschedule");
     866
     867        ostringstream out;
     868        out << Time()-stopwatch;
     869        Info(out);
     870
     871        return true;
     872    }
     873
     874    bool Schedule(const string &name, const double &ra, const double &dec)
     875    {
     876        try
     877        {
     878            return ScheduleImp(name, ra, dec);
     879        }
     880        catch (const exception &e)
     881        {
     882            Error(string("SQL query failed: ")+e.what());
     883            fConnection.disconnect();
     884            return false;
     885        }
     886    }
     887
     888    // ========================================================================
     889
     890
     891    // ========================================================================
     892
     893    bool CheckEventSize(size_t has, const char *name, size_t size)
     894    {
     895        if (has==size)
     896            return true;
     897
     898        ostringstream msg;
     899        msg << name << " - Received event has " << has << " bytes, but expected " << size << ".";
     900        Fatal(msg);
     901        return false;
     902    }
     903
     904    int ScheduleGCN(const EventImp &evt)
     905    {
     906        if (!CheckEventSize(evt.GetSize(), "ScheduleGCN", 2+4+3*8))
     907            return kSM_FatalError;
     908
     909        const ToO::DataGRB &grb = evt.Ref<ToO::DataGRB>();
     910
     911        const GCN::PaketType_t *ptr=GCN::kTypes;
     912        for (; ptr->type>=0 && ptr->type!=grb.type; ptr++);
     913        if (ptr->type==-1)
     914        {
     915            Warn("Unknown Packet Type: "+to_string(ptr->type));
     916            return GetCurrentState();
     917        }
     918
     919        ostringstream out;
     920        out << "Received: '" << ptr->name << "' ID=" << grb.type << " NUM=" << grb.trigid << " RA=" << grb.ra/15. << "h DEC=" << grb.dec << "\u00b0 ERR=" << grb.err;
     921
     922        Info(out);
     923        Info(ptr->description);
     924
     925        const CheckVisibility check(grb.ra, grb.dec);
     926
     927        Info(string("Source is")+(check.visible?" ":" NOT ")+"visible.");
     928
     929        Info(string("Sun altitude:   ")+(check.valid_sun    ?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.solarobj.fSunHrz.alt)+"\u00b0]");
     930        Info(string("Moon distance:  ")+(check.valid_moon   ?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.moon_dist)+"\u00b0]");
     931        Info(string("Zenith angle:   ")+(check.valid_zd     ?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.position.zd)+"\u00b0]");
     932        Info(string("Current:        ")+(check.valid_current?"OK    ":"failed")+" ["+Tools::Form("%5.1f", check.current)+" \u00b5A]");
     933
     934        const string name = ptr->instrument+"#"+to_string(grb.trigid);
     935        Info("Source name set to '"+name+"'");
     936
     937        /*
     938         * Swift: https://gcn.gsfc.nasa.gov/swift.html
     939         relevante Paket-Typen:
     940         60   BAT_Alert
     941         97   BAT_QL_Pos
     942         61   BAT_Pos
     943         62   BAT_Pos_Nack
     944         Es kann wohl vorkommen, dass ein Alert danach anders klassifiziert ist
     945         -> BAT_Trans (84) und es steht dass dann in der GRB notice eine catalog
     946         ID im comment steht. Da ist es mir noch nicht so ganz klar, woran man
     947         echte Alerts erkennt.
     948
     949         * Fermi: https://gcn.gsfc.nasa.gov/fermi.html
     950         relevante Paket-Typen:
     951         110   GBM_Alert           P        B               B                B&A
     952         111   GBM_Flt_Pos         P        B               B                B&A
     953         112   GBM_Gnd_Pos         P        B               B                B&A
     954         115   GBM_Final_Pos       P        B               B                B&A
     955         Bei GBM müssen wir usn überlegen wie wir es mit den Positionsupdates
     956         machen - das können auch mal ein paar Grad sein. siehe zB
     957         https://gcn.gsfc.nasa.gov/other/568833894.fermi
     958
     959         Wenn ich das richtig sehe, kommt bei GBM_Alert noch keine
     960         Position, sndern erst mit GBM_Flt_Pos (in dem Bsp kommt das 3 Sek
     961         später).
     962
     963         * INTEGRAL: https://gcn.gsfc.nasa.gov/integral.html
     964         wenn ich es richtig verstehe, sind die interessanten Alerts die WAKEUP
     965         (tw kommt ein Positionsupdate via OFFLINE oder REFINED). Es gibt auch
     966         noch WEAK, aber das sind scheinbar subthreshold alerts - würde ich
     967         jetzt erst mal weglassen. Im letzten Jahr gab es 8 WAKEUP alerts. 2017
     968         sogar nur 3, 2016 7, 2015 waren es einige mehr, aber das war V404_Cyg -
     969         in dem Fall steht dann aber ein Object-Name dabei - ansonsten steht as
     970         "Possbibly real GRB event" - zT steht auch, dass es coincident mit GBM
     971         events ist
     972
     973         * KONUS: https://gcn.gsfc.nasa.gov/konus.html
     974         Ist mir noch etwas unklar, was das genau ist - die schicken Lichtkurven
     975         von GRBs und Transients - muss man nochmal genauer schauen.
     976
     977         * MAXI: https://gcn.gsfc.nasa.gov/maxi.html
     978         wenn ich das richtig verstehe, sind das nicht nur GRBs -> müsste man
     979         nochmal genauer schauen bevor man da was implementiert
     980
     981         * AGILE: https://gcn.gsfc.nasa.gov/agile.html
     982         Wahrscheinlich eher nicht relevant, da steht was von 30min nach dem
     983         Burst kommt das 'Wakeup'. Die relevanten Paket-Typen wären 100
     984         (Wakeup_Pos), 101 (Ground_Pos) und 102 (Refined_Pos)
     985
     986         * AMON: https://gcn.gsfc.nasa.gov/amon.html
     987         sind bisher nur neutrinos - da macht MAGIC glaub ich schon automatic
     988         Follow-Up - müssen wir also kucken was Sinn macht
     989
     990         * Ligo-Virgo GW: https://gcn.gsfc.nasa.gov/lvc.html
     991         da ist natürlich die Error-Region groß - müsste man schauen, was man
     992         da will
     993
     994         Fazit: am relevantesten sind Fermi/GBM und Swift/BAT. Evtl könnte
     995         INTEGRAL noch Sinn machen und über AMON und LVC sollte man nachdenken,
     996         ob/was man haben will.
     997*/
     998
     999        Schedule(name, grb.ra, grb.dec);
     1000
     1001        return fConnection.connected() ? ToO::State::kConnected : ToO::State::kDisconnected;
     1002        //return GetCurrentState();
    6571003    }
    6581004
    6591005    int Execute()
    6601006    {
    661         switch (T::GetCurrentState())
    662         {
    663         case kSM_Scheduling:
     1007        Time now;
     1008        if (now>fKeepAliveDeadline)
     1009        {
     1010            static int ernum = 0;
    6641011            try
    6651012            {
    666                 return Schedule() ? T::kSM_Error : T::kSM_Ready;
     1013                // Unfortunately, reconnecting [Takes about 1s] is
     1014                // the only non-blocking way to ensure an open
     1015                // connection. Ping would be nice but is blocking.
     1016                fConnection = Database(fUri);
     1017                ernum = 0;
    6671018            }
    668             catch (const mysqlpp::Exception &e)
     1019            catch (const mysqlpp::ConnectionFailed &e)
    6691020            {
    670                 T::Error(string("MySQL: ")+e.what());
    671                 return T::kSM_Error;
     1021                if (ernum!=e.errnum())
     1022                    Error(e.what());
     1023                ernum = e.errnum();
    6721024            }
    6731025
    674             // This does an autmatic reset (FOR TESTING ONLY)
    675         case T::kSM_Error:
    676             return T::kSM_Ready;
    677         }
    678         return T::GetCurrentState();
    679     }
    680 
    681     int Transition(const Event &evt)
    682     {
    683         switch (evt.GetTargetState())
    684         {
    685         case kSM_Scheduling:
    686             if (evt.GetSize()>0)
    687                 fDBName = evt.GetText();
    688             break;
    689         }
    690 
    691         return evt.GetTargetState();
    692     }
     1026            fKeepAliveDeadline += boost::posix_time::seconds(fKeepAliveInterval);
     1027        }
     1028
     1029        return fConnection.connected() ? ToO::State::kConnected : ToO::State::kDisconnected;
     1030    }
     1031
     1032public:
     1033    StateMachineToO(ostream &out=cout) : StateMachineDim(out, "SCHEDULER"),
     1034        fConnection(""), fKeepAliveDeadline(Time())
     1035    {
     1036        AddStateName(ToO::State::kDisconnected, "Disconnected",
     1037                     "The Dim DNS is reachable, but the required subsystems are not available.");
     1038
     1039        AddStateName(ToO::State::kConnected, "Connected",
     1040                     "All needed subsystems are connected to their hardware, no action is performed.");
     1041
     1042        AddEvent("GCN", "S:1;I:1;D:1;D:1;D:1")
     1043            (bind(&StateMachineToO::ScheduleGCN, this, placeholders::_1))
     1044            (""
     1045             "|type[int16]:TypeID (see HeaderGCN.h)"
     1046             "|num[uint32]:Right ascension"
     1047             "|ra[double]:Right ascension"
     1048             "|dec[double]:Declination"
     1049             "|err[double]:Declination");
     1050    }
     1051
     1052    /*
     1053    bool GetConfig(Configuration &conf, const string &name, const string &sub, uint16_t &rc)
     1054    {
     1055        if (conf.HasDef(name, sub))
     1056        {
     1057            rc = conf.GetDef<uint16_t>(name, sub);
     1058            return true;
     1059        }
     1060
     1061        Error("Neither "+name+"default nor "+name+sub+" found.");
     1062        return false;
     1063    }*/
    6931064
    6941065    int EvalOptions(Configuration &conf)
    6951066    {
    696         fDatabase       = conf.Get<string>("schedule-database");
    697         fDurationCalRun = conf.Get<int>("duration-cal-run");
    698         fDurationPedRun = conf.Get<int>("duration-ped-run");
    699         fDurationRepos  = conf.Get<int>("duration-repos");
    700 
    701         if (!conf.Has("schedule"))
    702             return -1;
    703 
    704         fDBName = conf.Get<string>("schedule");
    705         return Schedule();
    706     }
    707 
     1067        fVerbose = !conf.Get<bool>("quiet");
     1068        fDryRun = conf.Get<bool>("dry-run");
     1069        fKeepAliveInterval = conf.Get<uint16_t>("keep-alive");
     1070        fUri = conf.Get<string>("schedule-database");
     1071        return -1;
     1072    }
    7081073};
    7091074
    7101075
    7111076// ------------------------------------------------------------------------
     1077
    7121078#include "Main.h"
    7131079
    714 template<class T, class S>
     1080template<class T>
    7151081int RunShell(Configuration &conf)
    7161082{
    717     return Main::execute<T, AutoScheduler<S>>(conf);
     1083    return Main::execute<T, StateMachineToO>(conf);
    7181084}
    7191085
    7201086void SetupConfiguration(Configuration &conf)
    7211087{
    722     po::options_description control("Scheduler options");
     1088    po::options_description control("Scheduler");
    7231089    control.add_options()
    724         ("no-dim",    po_switch(),    "Disable dim services")
    725         ("schedule-database", var<string>()
    726 #if BOOST_VERSION >= 104200
    727          ->required()
    728 #endif
    729                                            ,  "Database link as in\n\tuser:password@server[:port]/database.")
    730         ("schedule",          var<string>(),  "")
    731         ("mintime",           var<int>(),     "minimum observation time")
    732         ("duration-cal-run",  var<int>()
    733 #if BOOST_VERSION >= 104200
    734          ->required()
    735 #endif
    736                                            ,     "duration of calibration run [min]")
    737         ("duration-ped-run",  var<int>()
    738 #if BOOST_VERSION >= 104200
    739          ->required()
    740 #endif
    741                                            ,     "duration of pedestal run [min]")
    742         ("duration-repos",    var<int>()
    743 #if BOOST_VERSION >= 104200
    744          ->required()
    745 #endif
    746                                            ,     "duration of repositioning [min]")
     1090        ("quiet,q", po_bool(), "")
     1091        ("dry-run", po_bool(), "Switch off any alteration of the database")
     1092        ("keep-alive", var<uint16_t>(uint16_t(300)), "Interval in seconds to ping (reconnect) the database server")
     1093        ("schedule-database", var<string>(), "Database link as in\n\tuser:password@server[:port]/database[?compress=0|1].")
    7471094        ;
    7481095
    749     po::positional_options_description p;
    750     p.add("schedule", 1); // The first positional options
    751 
    7521096    conf.AddOptions(control);
    753     conf.SetArgumentPositions(p);
    7541097}
    7551098
     
    7571100{
    7581101    cout <<
    759         "The scheduler... TEXT MISSING\n"
     1102        "The scheduler program is able to schedule a new observation.\n"
    7601103        "\n"
    761         "The default is that the program is started without user intercation. "
    762         "All actions are supposed to arrive as DimCommands. Using the -c "
    763         "option, a local shell can be initialized. With h or help a short "
    764         "help message about the usuage can be brought to the screen.\n"
    765         "\n"
    766         "Usage: scheduler [-c type] [OPTIONS] <schedule-database>\n"
    767         "  or:  scheduler [OPTIONS] <schedule-database>\n";
     1104        "Usage: scheduler [-c type] [OPTIONS]\n"
     1105        "  or:  scheduler [OPTIONS]\n";
    7681106    cout << endl;
    7691107}
     
    7711109void PrintHelp()
    7721110{
    773     /* Additional help text which is printed after the configuration
    774      options goes here */
     1111    Main::PrintHelp<StateMachineToO>();
    7751112}
    7761113
     
    7821119    SetupConfiguration(conf);
    7831120
    784     po::variables_map vm;
    785     try
    786     {
    787         vm = conf.Parse(argc, argv);
    788     }
    789 #if BOOST_VERSION > 104000
    790     catch (po::multiple_occurrences &e)
    791     {
    792         cerr << "Program options invalid due to: " << e.what() << " of '" << e.get_option_name() << "'." << endl;
    793         return -1;
    794     }
    795 #endif
    796     catch (exception& e)
    797     {
    798         cerr << "Program options invalid due to: " << e.what() << endl;
    799         return -1;
    800     }
    801 
    802 //    try
    803     {
    804         // No console access at all
    805         if (!conf.Has("console"))
    806         {
    807             if (conf.Get<bool>("no-dim"))
    808                 return RunShell<LocalStream, StateMachine>(conf);
    809             else
    810                 return RunShell<LocalStream, StateMachineDim>(conf);
    811         }
    812         // Cosole access w/ and w/o Dim
    813         if (conf.Get<bool>("no-dim"))
    814         {
    815             if (conf.Get<int>("console")==0)
    816                 return RunShell<LocalShell, StateMachine>(conf);
    817             else
    818                 return RunShell<LocalConsole, StateMachine>(conf);
    819         }
    820         else
    821         {
    822             if (conf.Get<int>("console")==0)
    823                 return RunShell<LocalShell, StateMachineDim>(conf);
    824             else
    825                 return RunShell<LocalConsole, StateMachineDim>(conf);
    826         }
    827     }
    828 /*    catch (std::exception& e)
    829     {
    830         std::cerr << "Exception: " << e.what() << "\n";
    831     }*/
     1121    if (!conf.DoParse(argc, argv, PrintHelp))
     1122        return 127;
     1123
     1124    if (!conf.Has("console"))
     1125        return RunShell<LocalStream>(conf);
     1126
     1127    if (conf.Get<int>("console")==0)
     1128        return RunShell<LocalShell>(conf);
     1129    else
     1130        return RunShell<LocalConsole>(conf);
    8321131
    8331132    return 0;
    8341133}
     1134
Note: See TracChangeset for help on using the changeset viewer.