#include <stdio.h>
#include <string>
#include <iostream>
#include <arpa/inet.h>
#include "fmt/format.h"
#include "fmt/printf.h"
#include <fstream>
#include <map>
#include <bitset>
#include <vector>
#include <thread>
#include <signal.h>
#include <mutex>
#include "ext/powerblog/h2o-pp.hh"
#include "minicurl.hh"
#include <time.h>
#include "ubx.hh"
#include "bits.hh"
#include "minivec.hh"
#include "navmon.pb.h"
#include "ephemeris.hh"
#include "gps.hh"
#include "glonass.hh"
#include "beidou.hh"
#include "galileo.hh"
#include "tle.hh"
#include <optional>
#include "navmon.hh" 
#include <Tle.h>
#include "navparse.hh"
#include <fenv.h> 
#include "influxpush.hh"
#include "sbas.hh"
#include <sys/resource.h>

#include "CLI/CLI.hpp"
#include "gpscnav.hh"
#include "rtcm.hh"
#include "version.hh"
//#include "nequick.hh"

static char program[]="navparse";

using namespace std;

extern const char* g_gitHash;

struct ObserverFacts
{
  Point pos;
  double groundSpeed{-1};
  double accuracy{-1.0};
  string serialno;
  string hwversion;
  string swversion;
  string mods;
  string vendor;
  double clockOffsetNS{-1};
  double clockOffsetDriftNS{-1};
  double clockAccuracyNS{-1};
  double freqAccuracyPS{-1};
  time_t uptime;
  string githash;
  string owner;
  string remark;
  bool impinav{false};
  time_t impinavTime{0};
  time_t lastSeen{0};
};
std::map<int, ObserverFacts> g_srcfacts;

struct SBASAndReceiverStatus
{
  SBASState status;
  struct PerRecv
  {
    time_t last_seen{0};
  };
  map<int, PerRecv> perrecv;
};
typedef map<int, SBASAndReceiverStatus> sbas_t;
sbas_t g_sbas;
GetterSetter<sbas_t> g_sbaskeeper;

map<int, BeidouAlmanacEntry> g_beidoualma;
map<int, pair<GlonassMessage, GlonassMessage>> g_glonassalma;
map<int, GalileoMessage::Almanac> g_galileoalma;
map<int, GPSAlmanac> g_gpsalma;

GetterSetter<map<int, BeidouAlmanacEntry>> g_beidoualmakeeper;
GetterSetter<map<int, pair<GlonassMessage, GlonassMessage>>> g_glonassalmakeeper;
GetterSetter<map<int, GalileoMessage::Almanac>> g_galileoalmakeeper;
GetterSetter<map<int, GPSAlmanac>> g_gpsalmakeeper;

TLERepo g_tles;
struct GNSSReceiver
{
  Point position; 
};

double g_GSTUTCOffset, g_GSTGPSOffset, g_GPSUTCOffset, g_BeiDouUTCOffset, g_GlonassUTCOffset, g_GlonassGPSOffset;

/* 
The situation. We have a single ephemeris function that is able to operate on
Galileo, GPS1 and Beidou structs. It does so using functions like getT0e().
This ephemeris function also does speed and doppler. 

We have structs for all four GNSS.

All GNSS have ephemerides that are spread out over multiple messages. 
Because of that, we have to do some kind of atomic update.

We also have an SVStat that has some knowledge about IODs. 
It would be great is we could ask the svstat about "the latest ephemeris" 
and get a useful answer. 

More concretely, we could ask the svstat about the latest *position* or *speed*.
It would be easier to do that. There is no unified "ephemeris object" between all the GNSS.

We do need a unified "do we have a complete ephemeris method".

For Galileo this is messages 1, 2, 3 and 4
For GPS it is 2 and 3
For BeiDou it is message 2 and message 3 in succession (?)
   sow is in each message, forms a link
For GLONASS I don't really know

We also care about ephemeris discontinuities. 

Idea is that SVStat has four structs around, one for each GNSS, each containing "the lastest complete ephemeris".
   ephglomsg etc

   It also has infrastructure for storing the ephemerides as they are being assembled.

Open question: 

*/


// XXX conversion glonass??
int SVStat::wn() const
{
  if(gnss == 0)
    return gpsmsg.wn;
  else if(gnss == 2)
    return galmsg.wn;
  else if(gnss == 3)
    return beidoumsg.wn;
  else if(gnss == 6) {
    uint32_t glotime = glonassMessage.getGloTime(); // this starts GLONASS time at 31st of december 1995, 00:00 UTC
    return glotime / (7*86400);
  }
    
  return 0;
}

int SVStat::tow() const
{
  if(gnss == 0)
    return gpsmsg.tow;
  else if(gnss == 2)
    return galmsg.tow;
  else if(gnss == 3)
    return beidoumsg.sow;
  else if(gnss == 6) {
    uint32_t glotime = glonassMessage.getGloTime(); // this starts GLONASS time at 31st of december 1995, 00:00 UTC
    return glotime % (7*86400);
  }
  
  return 0;
}

const GPSLikeEphemeris& SVStat::liveIOD() const
{
  if(gnss == 0)
    return ephgpsmsg;
  else if(gnss == 2)
    return ephgalmsg;
  else if(gnss == 3)
    return ephBeidoumsg;

  throw std::runtime_error("Asked for GPS like ephemeris for gnss " + to_string(gnss));
}

const GPSLikeEphemeris& SVStat::prevIOD() const
{
  if(gnss == 0)
    return oldephgpsmsg;
  else if(gnss == 2)
    return oldephgalmsg;
  else if(gnss == 3)
    return oldephBeidoumsg;

  throw std::runtime_error("Asked for old-GPS like ephemeris for gnss " + to_string(gnss));
}


bool SVStat::completeIOD() const
{
  if(gnss == 6)
    return false;
  // yeah now what
  return true; 
}


double SVStat::getCoordinates(double tow, Point* p, bool quiet) const
{
  if(gnss == 6)
    return ::getCoordinates(tow, ephglomsg, p);

  // getCoordinates needs to be overloaded for GLONASS
  return ::getCoordinates(tow, liveIOD(), p, quiet);
  
}

double SVStat::getOldEphCoordinates(double tow, Point* p, bool quiet) const
{
  if(gnss == 6)
    return ::getCoordinates(tow, oldephglomsg, p);

  // getCoordinates needs to be overloaded for GLONASS
  return ::getCoordinates(tow, prevIOD(), p, quiet);
  
}


void SVStat::getSpeed(double tow, Vector* v) const
{
  return ::getSpeed(tow, liveIOD(), v);
}
DopplerData SVStat::doDoppler(double tow, const Point& us, double freq) const
{
  if(gnss == 6)
    return ::doDoppler(tow, us, ephglomsg, freq);
  else
    return ::doDoppler(tow, us, liveIOD(), freq);
}

double satUTCTime(const SatID& id);

void SVStat::reportNewEphemeris(const SatID& id, InfluxPusher& idb) 
{
  int ephage;
  if(gnss==6)
    ephage = ephAge(ephglomsg.getT0e(), oldephglomsg.getT0e());
  else
    ephage = ephAge(liveIOD().getT0e(), prevIOD().getT0e());
  
  Point p, oldp;
  getCoordinates(tow(), &p);
  getOldEphCoordinates(tow(), &oldp);

  double hours = ephage / 3600;
  double disco = Vector(p, oldp).length();
  //              cout<<id.first<<","<<id.second<<" discontinuity after "<< hours<<" hours: "<< disco <<endl;
  
  if(hours < 4) {
    latestDisco= disco;
    latestDiscoAge= ephage;
  }
  else
    latestDisco= -1;

  if(gnss==0) { // GPS
    const auto& eg = ephgpsmsg;

    
    idb.addValue(id, "ephemeris-actual", {
        {"iod", eg.getIOD()}, 
        {"t0e", eg.t0e},
          {"sqrta", eg.sqrtA},
            {"e", eg.e},
              {"cuc", eg.cuc},
                {"cus", eg.cus},
                  {"crc", eg.crc},
                    {"crs", eg.crs},
                      {"m0", eg.m0},
                        {"deltan", eg.deltan},
                          {"i0", eg.i0},
                            {"cic", eg.cic},
                              {"cis", eg.cis},
                                {"omegadot", eg.omegadot},
                                  {"omega0", eg.omega0},
                                    {"idot", eg.idot},
                                      {"af0", eg.af0},
                                        {"af1", eg.af1},
                                          {"af2", eg.af2},
                                            {"t0c", eg.t0c},
                                      {"omega", eg.omega}}, satUTCTime(id));

  }

  
  if(gnss==2) {
    const auto& eg = ephgalmsg;
    
    idb.addValue(id, "ephemeris-actual", {
        {"iod", eg.getIOD()}, 
        {"t0e", eg.t0e},
          {"sqrta", eg.sqrtA},
            {"e", eg.e},
              {"cuc", eg.cuc},
                {"cus", eg.cus},
                  {"crc", eg.crc},
                    {"crs", eg.crs},
                      {"m0", eg.m0},
                        {"deltan", eg.deltan},
                          {"i0", eg.i0},
                            {"cic", eg.cic},
                              {"cis", eg.cis},
                                {"omegadot", eg.omegadot},
                                  {"omega0", eg.omega0},
                                    {"idot", eg.idot},
                                      {"af0", eg.af0},
                                        {"af1", eg.af1},
                                          {"af2", eg.af2},
                                            {"t0c", eg.t0c},
                                      {"omega", eg.omega}}, satUTCTime(id));

  }
  
  if(hours < 24) {
    idb.addValue(id,  "eph-disco",
                 {{"meters", disco},
                     {"seconds", hours*3600.0},
                       {"oldx", oldp.x},
                         {"oldy", oldp.y},
                           {"oldz", oldp.z},
                             {"x", p.x},
                               {"y", p.y},
                                 {"z", p.z},
                                   {"iod", gnss == 6 ? -1.0 : 1.0*liveIOD().getIOD()},
                                     {"oldiod", gnss== 6 ? -1.0 : 1.0*prevIOD().getIOD()}}, satUTCTime(id));
  }


}



svstats_t g_svstats;

GetterSetter<svstats_t> g_statskeeper;

int latestWN(int gnssid, const svstats_t& stats)
{
  map<int, SatID> ages;
  for(const auto& s: stats)
    if(s.first.gnss == (unsigned int)gnssid)
      ages[7*s.second.wn()*86400 + s.second.tow()]= s.first;
  if(ages.empty())
    throw runtime_error("Asked for latest WN for "+to_string(gnssid)+": we don't know it yet ("+to_string(stats.size())+")");
  
  return stats.find(ages.rbegin()->second)->second.wn();
}

int latestTow(int gnssid, const svstats_t& stats)
{
  map<int, SatID> ages;
  for(const auto& s: stats)
    if(s.first.gnss == (unsigned int) gnssid)
      ages[7*s.second.wn()*86400 + s.second.tow()]= s.first;
  if(ages.empty())
    throw runtime_error("Asked for latest TOW for "+to_string(gnssid)+": we don't know it yet ("+to_string(stats.size())+")");
  return stats.find(ages.rbegin()->second)->second.tow();
}


// nanoseconds posix time from that gnss WN and TOW
int64_t nanoTime(int gnssid, int wn, double tow)
{
  int offset;
  if(gnssid == 0) // GPS
    offset = 315964800;
  if(gnssid == 2) // Galileo, 22-08-1999
    offset = 935280000;
  if(gnssid == 3) {// Beidou, 01-01-2006 - I think leap seconds count differently in Beidou!! XXX
    offset = 1136073600;
    return 1000000000ULL*(offset + wn * 7*86400 + tow - g_dtLSBeidou); 
  }
  if(gnssid == 6) { // GLONASS
    offset = 820368000;
    return 1000000000ULL*(offset + wn * 7*86400 + tow);  // no leap seconds in glonass
  }
  
  return 1000000000ULL*(offset + wn * 7*86400 + tow - g_dtLS); 
}

// same in seconds 
double satUTCTime(const SatID& id)
{
  return nanoTime(id.gnss, g_svstats[id].wn(), g_svstats[id].tow())/1000000000.0;
}


/* The GST start epoch is defined as 13 seconds before midnight between 21st August and
22nd August 1999, i.e. GST was equal to 13 seconds at 22nd August 1999 00:00:00 UTC.  */

std::string humanTime(int gnssid, int wn, int tow)
{
  time_t t = nanoTime(gnssid, wn, tow)/1000000000;

  struct tm tm;
  gmtime_r(&t, &tm);

  char buffer[80];
  strftime(buffer, sizeof(buffer), "%a, %d %b %Y %T %z", &tm);
  return buffer;
}

std::optional<double> getHzCorrection(time_t now, int src, unsigned int gnssid, unsigned int sigid, const svstats_t& svstats)
{
  
  std::optional<double> allHzCorr;
  double alltot=0;
  int allcount=0;
  //  cout<<"getHzCorrection called for src "<<src<<" gnss "<<gnssid <<" sigid "<< sigid <<endl;
  for(const auto& s: svstats) {
    if(s.first.gnss != gnssid)
      continue;
    if(s.first.sigid != sigid)
      continue;
    if(auto iter = s.second.perrecv.find(src); iter != s.second.perrecv.end() && now - iter->second.deltaHzTime < 60) {
      //      cout<<"  Found entry for SV "<<s.first.gnss<<","<<s.first.sv<<","<<s.first.sigid<<" from src "<<iter->first<<", deltaHz: "<<iter->second.deltaHz<< " age " << now - iter->second.deltaHzTime<<" db "<<iter->second.db<<endl;
      alltot+=iter->second.deltaHz;
      allcount++;
    }
  }
  if(allcount > 3) {
    allHzCorr = alltot/allcount;
    //    cout<<"Returning "<<*allHzCorr<<endl;
  }
  else
    ; //    cout<<"Not enough data"<<endl;
  return allHzCorr;
}



std::string humanBhs(int bhs)
{
  static vector<string> options{"ok", "OUT", "will be out of service", "test"};
  if(bhs >= (int)options.size()) {
    cerr<<"Asked for humanBHS "<<bhs<<endl;
    return "??";
  }
  return options.at(bhs);
}

void addHeaders(h2o_req_t* req)
{
  h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, 
                     NULL, H2O_STRLIT("max-age=3"));
  
  // Access-Control-Allow-Origin
  h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_ACCESS_CONTROL_ALLOW_ORIGIN, 
                 NULL, H2O_STRLIT("*"));

}

time_t getSatelliteUTC(svstats_t& svstats)
{
  time_t ret;
  try {
    ret=utcFromGST(latestWN(2, svstats), latestTow(2, svstats));
  }
  catch(...)
  {
    ret=utcFromGPS(latestWN(0, svstats), latestTow(0, svstats));
    // if this throws, we are done
  }
  return ret;
}

static double get_cpu_seconds_total()
{
  struct rusage usage;
  getrusage(RUSAGE_SELF, &usage);
  return usage.ru_utime.tv_sec + usage.ru_utime.tv_usec/1000000.0 + usage.ru_stime.tv_sec + usage.ru_stime.tv_usec/1000000.0;
}


void storeSelfStats(InfluxPusher& idb, time_t t)
{
  map<int, int> receivers;
  time_t now;
  try {
    now=getSatelliteUTC(g_svstats);
  }
  catch(...) {
    return;
  }
  map<SatID, int> svcount, sigcount;
  map<int, int> siggnsscount, svgnsscount;
  for(const auto& sv : g_svstats) {
    bool fresh=false;
    for(const auto& pr : sv.second.perrecv) {
      int age = now - pr.second.t;
      if(age < 30) {
        fresh= true;
        receivers[pr.first]++;
      }
    }
    if(fresh) {
      sigcount[sv.first]++;
      siggnsscount[sv.first.gnss]++;
      SatID id = sv.first;
      id.sigid=0;
      svcount[id]++;
    }
  }
  for(const auto& sv : svcount)
    svgnsscount[sv.first.gnss]++;

  
  vector<pair<string,InfluxPusher::var_t>> tags;
  idb.addValue(tags, "self", {
      {"measurements", idb.d_nummsmts},
        {"dedup-msmts", idb.d_numdedupmsmts},
          {"values", idb.d_numvalues},
            {"total-live-svs", (int64_t)svcount.size()},
              {"total-live-signals", (int64_t)sigcount.size()},
                {"gps-svs", svgnsscount[0]},
                  {"galileo-svs", svgnsscount[2]},
                    {"beidou-svs",svgnsscount[3]},
                      {"glonass-svs",svgnsscount[6]},
                        {"gps-sigs", siggnsscount[0]},
                          {"galileo-sigs", siggnsscount[2]},
                            {"beidou-sigs",  siggnsscount[3]},
                              {"glonass-sigs", siggnsscount[6]},
                                {"total-cpu-msec", 1000.0*get_cpu_seconds_total()},
                                {"total-live-receivers", (int64_t)receivers.size()}
    }, t);

  for(const auto& p : idb.d_msmtmap) {
    idb.addValue({{"metric", p.first}}, "msmts", {{"value", p.second}}, t);
  }
}


// GALILEO ONLY FOR NOW
void storeCoverageStats(InfluxPusher& idb, time_t t)
try {
  int tow;

  tow=latestTow(2, g_svstats);
  vector<Point> sats;

  for(const auto &g : g_galileoalma) {
    Point sat;
    getCoordinates(tow, g.second, &sat);
    
    if(g.first < 0)
      continue;
    SatID id{2,(uint32_t)g.first,1};
    const auto& svstat = g_svstats[id];
    if(svstat.completeIOD() && svstat.galmsg.sisa == 255) {
      continue;
    }
    if(svstat.galmsg.e1bhs || svstat.galmsg.e1bdvs) {
      continue;
    }
    sats.push_back(sat);
  }

  vector<SatID> aux{{2, 14, 1}, {2,18,1}};
  for(const auto& id : aux) {
    if(!g_svstats.count(id))
      continue;
    
    const auto& svstat = g_svstats[id];

    if(svstat.completeIOD() && svstat.galmsg.sisa == 255) {
      continue;
    }
    if(svstat.galmsg.e1bhs || svstat.galmsg.e1bdvs) {
      continue;
    }
    Point sat;
    getCoordinates(tow, svstat.galmsg, &sat);
    sats.push_back(sat);
  }
  
  //  cout<<endl;
  auto cov = emitCoverage(sats);
  int cells=0;
  int pdopexceeds5=0, pdopexceeds10=0, pdopexceeds20=0;
  int covlow5=0, covlow10=0, covlow20=0;
  for(const auto& latvect : cov) {
    for(const auto& longpair : latvect.second) {
      cells++;
      if(get<4>(longpair) >= 6.0 || get<4>(longpair) < 0.0 )
        pdopexceeds5++;
      if(get<5>(longpair) >= 6.0 || get<5>(longpair) < 0.0 )
        pdopexceeds10++;
      if(get<6>(longpair) >= 6.0 || get<6>(longpair) < 0.0 )
        pdopexceeds20++;

      if(get<1>(longpair) < 4)
        covlow5++;
      if(get<2>(longpair) < 4)
        covlow10++;
      if(get<3>(longpair) < 4)
        covlow20++;

      
      //            else
      //cout<<get<6>(longpair) << endl;
    }
  }
  /*
  fmt::printf("At %s, %.2f%% (%d) of %d cells exceeded PDOP 6 for 5 degrees horizon (%d sats)\n", humanTime(t), 100.0*pdopexceeds5/cells, pdopexceeds5, cells, sats.size());
  fmt::printf("At %s, %.2f%% (%d) of %d cells exceeded PDOP 6 for 10 degrees horizon (%d sats)\n", humanTime(t), 100.0*pdopexceeds10/cells, pdopexceeds10, cells, sats.size());
  fmt::printf("At %s, %.2f%% (%d) of %d cells exceeded PDOP 6 for 20 degrees horizon (%d sats)\n", humanTime(t), 100.0*pdopexceeds20/cells, pdopexceeds20, cells, sats.size());

  fmt::printf("At %s, %.2f%% (%d) of %d cells have less than 4 sats in view for 5 degrees horizon (%d sats)\n", humanTime(t), 100.0*covlow5/cells, covlow5, cells, sats.size());
  fmt::printf("At %s, %.2f%% (%d) of %d cells have less than 4 sats in view for 10 degrees horizon (%d sats)\n", humanTime(t), 100.0*covlow10/cells, covlow10, cells, sats.size());
  fmt::printf("At %s, %.2f%% (%d) of %d cells have less than 4 sats in view for 20 degrees horizon (%d sats)\n", humanTime(t), 100.0*covlow20/cells, covlow20, cells, sats.size());
  */
  idb.addValue({{"sigid", 1},{"gnss", 2}}, "quality", {
      {"pdop5perc", 100.0*pdopexceeds5/cells},
        {"pdop10perc", 100.0*pdopexceeds10/cells},
          {"pdop20perc", 100.0*pdopexceeds20/cells},
            {"covlow5", 100.0*covlow5/cells},
              {"covlow10", 100.0*covlow10/cells},
                {"covlow20", 100.0*covlow20/cells},
                  {"sats", (int)sats.size()}
    }, t);
  
}
catch(std::exception&e) {
  cout<<"Error with coverage: "<<e.what()<<endl;
}


int main(int argc, char** argv)
try
{
  bool doVERSION{false};

  CLI::App app(program);
  string localAddress("127.0.0.1:29599");
  string htmlDir("./html");
  string influxDBName("null");
  bool doGalileoReportSpeedup{false};
  bool doLogRFData{false};
  app.add_flag("--version", doVERSION, "show program version and copyright");
  app.add_flag("--log-rf-data", doLogRFData, "store per station RF/correlator data");
  app.add_flag("--gal-report-speedup", doGalileoReportSpeedup, "skip debugging data, glonass, beidou, SBAS");
  app.add_option("--bind,-b", localAddress, "Address to bind to");
  app.add_option("--html", htmlDir, "Where to source the HTML & JavaScript");
  app.add_option("--influxdb", influxDBName, "Name of influxdb database");
    
  try {
    app.parse(argc, argv);
  } catch(const CLI::Error &e) {
    return app.exit(e);
  }

  if(doVERSION) {
    showVersion(program, g_gitHash);
    exit(0);
  }

  //  feenableexcept(FE_DIVBYZERO | FE_INVALID | FE_OVERFLOW ); 

  
  // g_tles.parseFile("active.txt");

  g_tles.parseFile("galileo.txt");
  g_tles.parseFile("glo-ops.txt");
  g_tles.parseFile("gps-ops.txt");
  g_tles.parseFile("beidou.txt");

  
  signal(SIGPIPE, SIG_IGN);
  InfluxPusher idb(influxDBName);
  MiniCurl::init();
  
  H2OWebserver h2s("galmon");
  
  h2s.addHandler("/global.json", [](auto handler, auto req) {
      addHeaders(req);
      
      nlohmann::json ret = nlohmann::json::object();
      auto svstats = g_statskeeper.get();
      ret["leap-seconds"] = g_dtLS;
      try {
        // if we haven't seen galileo, we have no idea
        ret["last-seen"]=utcFromGST(latestWN(2, svstats), latestTow(2, svstats));
      }
      catch(...)
        {
           try {
             ret["last-seen"]=utcFromGPS(latestWN(0, svstats), latestTow(0, svstats));        
           }
           catch(...)
           {}
        }
      
      ret["gst-utc-offset-ns"] = g_GSTUTCOffset;
      ret["gst-gps-offset-ns"] = g_GSTGPSOffset;
      ret["gps-utc-offset-ns"] = g_GPSUTCOffset;
      ret["beidou-utc-offset-ns"] = g_BeiDouUTCOffset;
      ret["glonass-utc-offset-ns"] = g_GlonassUTCOffset;
      ret["glonass-gps-offset-ns"] = g_GlonassGPSOffset;

      map<int, int> siggnsscount, svgnsscount;
      map<SatID, int> svcount, sigcount;
      map<int, int> receivers;
      time_t now=time(0);
      for(const auto& sv : svstats) {
        bool fresh=false;
        for(const auto& pr : sv.second.perrecv) {
          int age = now - pr.second.t;
          if(age < 30) {
            fresh= true;
            receivers[pr.first]++;
          }
        }
        if(fresh) {

          sigcount[sv.first]++;
          siggnsscount[sv.first.gnss]++;
          SatID id = sv.first;
          id.sigid=0;
          svcount[id]++;
        }
      }
      for(const auto& sv : svcount)
        svgnsscount[sv.first.gnss]++;

      ret["total-live-receivers"] = receivers.size();
      ret["total-live-svs"] = svcount.size();
      ret["total-live-signals"] = sigcount.size();
      ret["gps-svs"] = svgnsscount[0];
      ret["galileo-svs"] = svgnsscount[2];
      ret["beidou-svs"] = svgnsscount[3];
      ret["glonass-svs"] = svgnsscount[6];

      ret["gps-sigs"] = siggnsscount[0];
      ret["galileo-sigs"] = siggnsscount[2];
      ret["beidou-sigs"] = siggnsscount[3];
      ret["glonass-sigs"] = siggnsscount[6];

      return ret;
    });

  h2s.addHandler("/almanac.json", [](auto handler, auto req) {
      addHeaders(req);
      
      auto beidoualma = g_beidoualmakeeper.get();
      auto svstats = g_statskeeper.get();
      nlohmann::json ret = nlohmann::json::object();
      for(const auto& ae : beidoualma) {
        
        nlohmann::json item  = nlohmann::json::object();
        item["gnssid"]=3;
        if(ae.second.alma.getT0e() > 7*86400)
          continue;
        Point sat;
        getCoordinates(latestTow(3, svstats), ae.second.alma, &sat);
        item["eph-ecefX"]= sat.x/1000;
        item["eph-ecefY"]= sat.y/1000;
        item["eph-ecefZ"]= sat.z/1000;

        auto longlat = getLongLat(sat.x, sat.y, sat.z);
        item["eph-longitude"] = 180*longlat.first/M_PI;
        item["eph-latitude"]= 180*longlat.second/M_PI;
        item["t0e"] = ae.second.alma.getT0e();
        item["t"]= ephAge(ae.second.alma.getT0e(), latestTow(3, svstats))/86400.0;
        item["inclination"] = 180 * ae.second.alma.getI0() /M_PI;

        item["observed"]=false;
        if(auto iter = svstats.find({3, (uint32_t)ae.first, 0}); iter != svstats.end()) {
          if(time(0) - nanoTime(3, iter->second.wn(), iter->second.tow())/1000000000 < 300)
            item["observed"] = true;
        }
        
        if(ephAge(ae.second.alma.getT0e(), latestTow(3, svstats)) < 0) {
          auto match = g_tles.getBestMatch(nanoTime(3, latestWN(3, svstats), latestTow(3, svstats))/1000000000.0,
                                         sat.x, sat.y, sat.z);

          if(match.distance < 200000) {
            item["best-tle"] = match.name;
            item["best-tle-norad"] = match.norad;
            item["best-tle-int-desig"] = match.internat;
            item["best-tle-dist"] = match.distance/1000.0;
            
            item["tle-ecefX"] = match.ecefX/1000;
            item["tle-ecefY"] = match.ecefY/1000;
            item["tle-ecefZ"] = match.ecefZ/1000;
            
            item["tle-eciX"] = match.eciX/1000;
            item["tle-eciY"] = match.eciY/1000;
            item["tle-eciZ"] = match.eciZ/1000;
            
            item["tle-latitude"] = 180*match.latitude/M_PI;
            item["tle-longitude"] = 180*match.longitude/M_PI;
            item["tle-altitude"] = match.altitude;
          }
        }
        auto name = fmt::sprintf("C%02d", ae.first);
        item["name"]=name;
        ret[name] = item;
      }

      auto glonassalma = g_glonassalmakeeper.get();
      for(const auto& ae : glonassalma) {
        nlohmann::json item  = nlohmann::json::object();

        // ae.second.first -> even ae.second.sceond -> odd 
        item["gnssid"]=6;
        item["e"] = ae.second.first.getE();
        item["inclination"] = 180 * ae.second.first.getI0() /M_PI;
        item["health"] = ae.second.first.CnA;
        item["tlambdana"] = ae.second.second.gettLambdaNa();
        item["lambdana"] = ae.second.second.getLambdaNaDeg();
        item["hna"] = ae.second.second.hna;

        item["observed"] = false;
        for(uint32_t sigid : {0,1,2}) { // XXX SIGIDS
          if(auto iter = svstats.find({6, (uint32_t)ae.first, sigid}); iter != svstats.end()) {
            if(time(0) - nanoTime(6, iter->second.wn(), iter->second.tow())/1000000000 < 300) {
              item["observed"] = true;
              auto longlat = getLongLat(iter->second.glonassMessage.x, iter->second.glonassMessage.y, iter->second.glonassMessage.z);
              item["eph-longitude"] = 180*longlat.first/M_PI;
              item["eph-latitude"]= 180*longlat.second/M_PI;
              break;
            }
            
          }
        }


        auto name = fmt::sprintf("R%02d", ae.first);
        item["name"]=name;
        ret[name] = item;
      }

      auto galileoalma = g_galileoalmakeeper.get();
      for(const auto& ae : galileoalma) {
        nlohmann::json item  = nlohmann::json::object();
        item["gnssid"]=2;
        item["e"] = ae.second.getE();
        item["e1bhs"] = ae.second.e1bhs;
        item["e5bhs"] = ae.second.e5bhs;
        item["t0e"] = ae.second.getT0e();
        item["t"]= ephAge(ae.second.getT0e(), latestTow(2, svstats))/86400.0;
        item["eph-age"] = ephAge(latestTow(2, svstats), ae.second.getT0e());
        item["i0"] = 180.0 * ae.second.getI0()/ M_PI;
        item["inclination"] = 180 * ae.second.getI0() /M_PI;

        item["omega"] = ae.second.getOmega();
        item["sqrtA"]= ae.second.getSqrtA();
        item["M0"] = ae.second.getM0();
        item["delta-n"] = ae.second.getDeltan();
        item["omega-dot"] = ae.second.getOmegadot();
        item["omega0"] = ae.second.getOmega0();
        item["idot"] = ae.second.getIdot();
        item["t0e"] = ae.second.getT0e();

        Point sat;
        double E=getCoordinates(latestTow(2, svstats), ae.second, &sat);
        item["E"]=E;
        item["eph-ecefX"]= sat.x/1000;
        item["eph-ecefY"]= sat.y/1000;
        item["eph-ecefZ"]= sat.z/1000;

        auto longlat = getLongLat(sat.x, sat.y, sat.z);
        item["eph-longitude"] = 180*longlat.first/M_PI;
        item["eph-latitude"]= 180*longlat.second/M_PI;


        item["observed"] = false;
        for(uint32_t sigid : {0,1,5}) {
          if(auto iter = svstats.find({2, (uint32_t)ae.first, sigid}); iter != svstats.end()) {

            if(iter->second.completeIOD()) { 
              item["sisa"] = iter->second.galmsg.sisa;
            }
            // if we hit an 'observed', stop trying sigids
            if(time(0) - nanoTime(2, iter->second.wn(), iter->second.tow())/1000000000 < 300) {
              item["observed"] = true;
              break;
            }

            
          }
        }

        
        auto match = g_tles.getBestMatch(nanoTime(2, latestWN(2, svstats), latestTow(2, svstats))/1000000000.0,
                                         sat.x, sat.y, sat.z);
        
        if(match.distance < 200000) {
          item["best-tle"] = match.name;
          item["best-tle-norad"] = match.norad;
          item["best-tle-int-desig"] = match.internat;
          item["best-tle-dist"] = match.distance/1000.0;
          
          item["tle-ecefX"] = match.ecefX/1000;
          item["tle-ecefY"] = match.ecefY/1000;
          item["tle-ecefZ"] = match.ecefZ/1000;
          
          item["tle-eciX"] = match.eciX/1000;
          item["tle-eciY"] = match.eciY/1000;
          item["tle-eciZ"] = match.eciZ/1000;
          
          item["tle-latitude"] = 180*match.latitude/M_PI;
          item["tle-longitude"] = 180*match.longitude/M_PI;
          item["tle-altitude"] = match.altitude;
        }
        auto name = fmt::sprintf("E%02d", ae.first);
        item["name"]= name;
        ret[name] = item;
      }

      auto gpsalma = g_gpsalmakeeper.get();
      for(const auto& ae : gpsalma) {
        nlohmann::json item  = nlohmann::json::object();
        item["gnssid"]=0;
        item["e"] = ae.second.getE();
        item["health"] = ae.second.health;
        item["t0e"] = ae.second.getT0e();
        item["t"]= ephAge(ae.second.getT0e(), latestTow(0, svstats))/86400.0;
        item["eph-age"] = ephAge(latestTow(0, svstats), ae.second.getT0e());
        item["i0"] = 180.0 * ae.second.getI0()/ M_PI;
        item["inclination"] = 180 * ae.second.getI0() /M_PI;
        Point sat;
        getCoordinates(latestTow(0, svstats), ae.second, &sat);
        item["eph-ecefX"]= sat.x/1000;
        item["eph-ecefY"]= sat.y/1000;
        item["eph-ecefZ"]= sat.z/1000;

        auto longlat = getLongLat(sat.x, sat.y, sat.z);
        item["eph-longitude"] = 180*longlat.first/M_PI;
        item["eph-latitude"]= 180*longlat.second/M_PI;

        item["observed"] = false;
        for(uint32_t sigid : {0,1,4}) {
          if(auto iter = svstats.find({0, (uint32_t)ae.first, sigid}); iter != svstats.end()) {
            if(time(0) - nanoTime(0, iter->second.wn(), iter->second.tow())/1000000000 < 300)
              item["observed"] = true;
          }
        }

        
        auto match = g_tles.getBestMatch(nanoTime(0, latestWN(0, svstats), latestTow(0, svstats))/1000000000.0,
                                         sat.x, sat.y, sat.z);
        
        if(match.distance < 200000) {
          item["best-tle"] = match.name;
          item["best-tle-norad"] = match.norad;
          item["best-tle-int-desig"] = match.internat;
          item["best-tle-dist"] = match.distance/1000.0;
          
          item["tle-ecefX"] = match.ecefX/1000;
          item["tle-ecefY"] = match.ecefY/1000;
          item["tle-ecefZ"] = match.ecefZ/1000;
          
          item["tle-eciX"] = match.eciX/1000;
          item["tle-eciY"] = match.eciY/1000;
          item["tle-eciZ"] = match.eciZ/1000;
          
          item["tle-latitude"] = 180*match.latitude/M_PI;
          item["tle-longitude"] = 180*match.longitude/M_PI;
          item["tle-altitude"] = match.altitude;
        }
        auto name = fmt::sprintf("G%02d", ae.first);
        item["name"]=name;
        ret[name] = item;
      }
      
      return ret;
    });

  h2s.addHandler("/observers.json", [](auto handler, auto req) {
      addHeaders(req);
      
      nlohmann::json ret = nlohmann::json::array();
      for(const auto& src : g_srcfacts) {
        nlohmann::json obj;
        obj["id"] = src.first;
        auto latlonh = ecefToWGS84(src.second.pos.x, src.second.pos.y, src.second.pos.z);
        get<0>(latlonh) *= 180.0/M_PI;
        get<1>(latlonh) *= 180.0/M_PI;
        get<0>(latlonh) = ((int)(10*get<0>(latlonh)))/10.0;
        get<1>(latlonh) = ((int)(10*get<1>(latlonh)))/10.0;

        get<2>(latlonh) = ((int)(10*get<2>(latlonh)))/10.0;

        obj["latitude"] = get<0>(latlonh);
        obj["longitude"] = get<1>(latlonh);
        obj["last-seen"] = src.second.lastSeen;
        obj["ground-speed"] = src.second.groundSpeed;
        obj["swversion"] = src.second.swversion;
        obj["hwversion"] = src.second.hwversion;
        obj["mods"] = src.second.mods;
        obj["serialno"] = src.second.serialno;
        obj["clockoffsetns"]= src.second.clockOffsetNS;
        obj["clockdriftns"]= src.second.clockOffsetDriftNS;
        obj["clockacc"]= src.second.clockAccuracyNS;
        obj["freqacc"]= src.second.freqAccuracyPS;
        obj["uptime"]= src.second.uptime;
        obj["githash"]= src.second.githash;
        obj["owner"]= src.second.owner;
        obj["vendor"]= src.second.vendor;
        obj["remark"]= src.second.remark;
        if(src.second.impinavTime >= 0 && (time(0) + 10) - src.second.impinavTime < 86400)
          obj["impinav"]=true;
        else
          obj["impinav"]=false;
          
        obj["acc"] = src.second.accuracy;
        obj["h"] = get<2>(latlonh);
        auto svstats = g_statskeeper.get();
        nlohmann::json svs = nlohmann::json::object();

        for(const auto& sv : svstats) {
          if(auto iter = sv.second.perrecv.find(src.first); iter != sv.second.perrecv.end()) {
            if(iter->second.db > 0 &&  time(0) - iter->second.t < 120) {
              nlohmann::json svo = nlohmann::json::object();
              svo["db"] = iter->second.db;

              svo["elev"] = roundf(10.0*iter->second.el)/10.0;
              svo["azi"] = roundf(10.0*iter->second.azi)/10.0;

              Point sat;
              
              if((sv.first.gnss == 0 || sv.first.gnss == 2) && sv.second.completeIOD()) {
                svo["delta_hz"] = truncPrec(iter->second.deltaHz, 2);
                auto hzCorrection = getHzCorrection(time(0), src.first , sv.first.gnss, sv.first.sigid, svstats);
                if(hzCorrection)
                  svo["delta_hz_corr"] = truncPrec(iter->second.deltaHz - *hzCorrection, 2);
                
                sv.second.getCoordinates(latestTow(sv.first.gnss, svstats), & sat);
              }
              if(sv.first.gnss == 3 && sv.second.ephBeidoumsg.sow >= 0 && sv.second.ephBeidoumsg.sqrtA != 0) {
                getCoordinates(latestTow(sv.first.gnss, svstats), sv.second.ephBeidoumsg, &sat);
              }
              if(sv.first.gnss == 6 && sv.second.wn() > 0) {
		getCoordinates(latestTow(6, svstats), sv.second.glonassMessage, &sat);
              }
              if(sat.x) {
                Point our = g_srcfacts[iter->first].pos;
                svo["elev"] = roundf(10.0*getElevationDeg(sat, our))/10.0;
                svo["azi"] = roundf(10.0*getAzimuthDeg(sat, our))/10.0;
              }

              svo["prres"] = truncPrec(iter->second.prres, 2);
              svo["qi"] = iter->second.qi;
              svo["used"] = iter->second.used;
              svo["age-s"] = time(0) - iter->second.t;
              svo["last-seen"] = iter->second.t;
              svo["gnss"] = sv.first.gnss;
              svo["sv"] = sv.first.sv;
              svo["sigid"] = sv.first.sigid;
              svo["fullName"] = makeSatIDName(sv.first);
              svo["name"] = makeSatPartialName(sv.first);

              svs[makeSatIDName(sv.first)] = svo;
            }

          }
        }
        obj["svs"]=svs;
        
        ret.push_back(obj);
      }
      return ret;
    });

  h2s.addHandler("/sv.json", [](auto handler, auto req) {
      addHeaders(req);
      string_view path = convert(req->path);
      nlohmann::json ret = nlohmann::json::object();

      SatID id;
      auto pos = path.find("sv=");
      if(pos == string::npos) {
        return ret;
      }
      id.sv = atoi(&path[0] + pos+3);

      pos = path.find("gnssid=");
      if(pos == string::npos) {
        return ret;
      }
      id.gnss = atoi(&path[0]+pos+7);
      
      id.sigid = 1;
      pos = path.find("sigid=");
      if(pos != string::npos) {
        id.sigid = atoi(&path[0]+pos+6);
      }
      
      auto svstats = g_statskeeper.get();

      ret["sv"] = id.sv;
      ret["gnssid"] =id.gnss;
      ret["sigid"] = id.sigid;
      auto iter = svstats.find(id);
      if(iter == svstats.end())
        return ret;
      const auto& s= iter->second;
      // XXX CONVERSION
      /*
      */
      if(id.gnss == 2) {
        ret["sf1"] = s.galmsg.sf1;
        ret["sf2"] = s.galmsg.sf2;
        ret["sf3"] = s.galmsg.sf3;
        ret["sf4"] = s.galmsg.sf4;
        ret["sf5"] = s.galmsg.sf5;
        ret["BGDE1E5a"] = s.galmsg.BGDE1E5a;
        ret["BGDE1E5b"] = s.galmsg.BGDE1E5b;
        ret["e5bdvs"]=s.galmsg.e5bdvs;
        ret["e1bdvs"]=s.galmsg.e1bdvs;
        ret["e5bhs"]=s.galmsg.e5bhs;
        ret["e1bhs"]=s.galmsg.e1bhs;
	ret["ai0"] = s.galmsg.ai0;
	ret["ai1"] = s.galmsg.ai1;
	ret["ai2"] = s.galmsg.ai2;
	ret["dtLS"] = s.galmsg.dtLS;
	ret["dtLSF"] = s.galmsg.dtLSF;
	ret["wnLSF"] = s.galmsg.wnLSF;
	ret["dn"] = s.galmsg.dn;
	ret["a0"] = s.galmsg.a0;
	ret["a1"] = s.galmsg.a1;
	ret["a0g"] = s.galmsg.a0g;
	ret["a1g"] = s.galmsg.a1g;
	ret["t0c"] = s.galmsg.getT0c();
	ret["af0"] = s.galmsg.af0;
	ret["af1"] = s.galmsg.af1;
	ret["af2"] = s.galmsg.af2;
        ret["af0red"] = s.galmsg.af0red;
        ret["af1red"] = s.galmsg.af1red;

      }
      // XXX CONVERSION
      /*
      */
      ret["wn"] = s.wn();
      ret["tow"] = s.tow();
      // XXX CONVERSION
      /*
      */

      if(id.gnss == 3 && svstats[id].ephBeidoumsg.sow >= 0 && svstats[id].ephBeidoumsg.sqrtA != 0) {
        const auto& iod = svstats[id].ephBeidoumsg;
        ret["e"] = iod.getE();
        ret["omega"] = iod.getOmega();
        ret["sqrtA"]= iod.getSqrtA();
        ret["M0"] = iod.getM0();
        ret["i0"] = iod.getI0();
        ret["delta-n"] = iod.getDeltan();
        ret["omega-dot"] = iod.getOmegadot();
        ret["omega0"] = iod.getOmega0();
        ret["idot"] = iod.getIdot();
        ret["t0e"] = iod.getT0e();
        ret["t0c"] = iod.getT0c();
        ret["cuc"] = iod.getCuc();
        ret["cus"] = iod.getCus();
        ret["crc"] = iod.getCrc();
        ret["crs"] = iod.getCrs();
        ret["cic"] = iod.getCic();
        ret["cis"] = iod.getCis();
        //        ret["sisa"] = iod.sisa;
        ret["af0"] = iod.a0;
        ret["af1"] = iod.a1;
        ret["af2"] = iod.a2;
        Point p;
        s.getCoordinates(s.tow(), &p);
        ret["x"] = p.x;
        ret["y"] = p.y;
        ret["z"] = p.z;
        auto longlat = getLongLat(p.x, p.y, p.z);
        ret["longitude"] = 180.0*longlat.first/M_PI;
        ret["latitude"] = 180.0*longlat.second/M_PI;

      }
      else if(svstats[id].completeIOD()) {
        const auto& iod =  svstats[id].liveIOD();
        ret["iod"]= svstats[id].liveIOD().getIOD();
        ret["e"] = iod.getE();
        ret["omega"] = iod.getOmega();
        ret["sqrtA"]= iod.getSqrtA();
        ret["M0"] = iod.getM0();
        ret["i0"] = iod.getI0();
        ret["delta-n"] = iod.getDeltan();
        ret["omega-dot"] = iod.getOmegadot();
        ret["omega0"] = iod.getOmega0();
        ret["idot"] = iod.getIdot();
        ret["t0e"] = iod.getT0e();
        // XXX conversion 
        //        ret["t0c"] = iod.getT0c();
        ret["cuc"] = iod.getCuc();
        ret["cus"] = iod.getCus();
        ret["crc"] = iod.getCrc();
        ret["crs"] = iod.getCrs();
        ret["cic"] = iod.getCic();
        ret["cis"] = iod.getCis();
        // XXX conversion
        /*
        ret["sisa"] = iod.sisa;
        ret["af0"] = iod.af0;
        ret["af1"] = iod.af1;
        ret["af2"] = iod.af2;
        */
        Point p;
        s.getCoordinates(s.tow(), &p);
        ret["x"] = p.x;
        ret["y"] = p.y;
        ret["z"] = p.z;
        auto longlat = getLongLat(p.x, p.y, p.z);
        ret["longitude"] = 180.0*longlat.first/M_PI;
        ret["latitude"] = 180.0*longlat.second/M_PI;
       
      }

      nlohmann::json recvs = nlohmann::json::object();

      for(const auto& perrecv : s.perrecv) {
        nlohmann::json recv = nlohmann::json::object();
        recv["db"] = perrecv.second.db;
        recv["qi"] = perrecv.second.qi;
        recv["used"] = perrecv.second.used;
        recv["prres"] = truncPrec(perrecv.second.prres, 2);
        recv["elev"] = perrecv.second.el;
        recv["last-seen-s"] = time(0) - perrecv.second.t;
        recvs[std::to_string(perrecv.first)] = recv;
                
      }
      ret["recvs"]=recvs;
      return ret;
    }
    );

  h2s.addHandler("/cov.json", [](auto handler, auto req) {
      addHeaders(req);
      vector<Point> sats;
      auto galileoalma = g_galileoalmakeeper.get();
      auto gpsalma = g_gpsalmakeeper.get();
      auto beidoualma = g_beidoualmakeeper.get();
      auto svstats = g_statskeeper.get();
      //  cout<<"pseudoTow "<<pseudoTow<<endl;
      string_view path = convert(req->path);

      bool doGalileo{true}, doGPS{false}, doBeidou{false}, doGlonass{false};
      auto pos = path.find("gps=");
      if(pos != string::npos) {
        doGPS = (path[pos+4]=='1');
      }
      pos = path.find("galileo=");
      if(pos != string::npos) {
        doGalileo = (path[pos+8]=='1');
      }
      pos = path.find("beidou=");
      if(pos != string::npos) {
        doBeidou = (path[pos+7]=='1');
      }
      pos = path.find("glonass=");
      if(pos != string::npos) {
        doGlonass = (path[pos+8]=='1');
      }
      
      if(doGalileo)
      for(const auto &g : galileoalma) {
        Point sat;
        getCoordinates(latestTow(2, svstats), g.second, &sat);
        
        if(g.first < 0)
          continue;
        SatID id{2,(uint32_t)g.first,1};
        if(svstats[id].completeIOD() && svstats[id].galmsg.sisa == 255) {
          continue;
        }
        if(svstats[id].galmsg.e1bhs || svstats[id].galmsg.e1bdvs)
          continue;
        sats.push_back(sat);
      }

      if(doGPS)
      for(const auto &g : gpsalma) {
        Point sat;
        getCoordinates(latestTow(0, svstats), g.second, &sat);
        
        if(g.first < 0)
          continue;
        SatID id{0,(uint32_t)g.first,0};
        if(svstats[id].completeIOD() && svstats[id].gpsmsg.ura == 16) {
          //          cout<<"Skipping G"<<id.sv<<" because of URA"<<endl;
          continue;
        }
        if(svstats[id].gpsmsg.gpshealth) {
          //          cout<<"Skipping G"<<id.sv<<" because of health"<<endl;
          continue;
        }
        sats.push_back(sat);
      }

      if(doBeidou)
      for(const auto &g : beidoualma) {
        Point sat;
        getCoordinates(latestTow(3, svstats), g.second.alma, &sat);
        
        if(g.first < 0)
          continue;
        SatID id{3,(uint32_t)g.first,0};
        /*
        if(svstats[id].completeIOD() && svstats[id].ura == 16) {
          cout<<"Skipping G"<<id.sv<<" because of URA"<<endl;
          continue;
        }
        */
        if(svstats[id].gpsmsg.gpshealth) {
          //          cout<<"Skipping C"<<id.sv<<" because of health"<<endl;
          continue;
        }
        sats.push_back(sat);
      }

      // fake almanac from svstats -- to avoid ejecting Glonasses to another galaxy due
      // to excessive extrapolation, they freeze where they were last seen.
      if(doGlonass)
      for(const auto &s : svstats) {
	if(s.first.gnss == 6 && s.second.wn() > 0) {
	  Point sat;
	  getCoordinates(s.second.tow(), s.second.glonassMessage, &sat);
	  if(svstats[s.first].glonassMessage.Bn & 7) {
	    continue;
	  }
	  sats.push_back(sat);
	}
      }
      
      
      auto cov = emitCoverage(sats);
      auto ret = nlohmann::json::array();

      // ret = 
      // [ [90, [[-180, 3,2,1], [-179, 3,2,1], ... [180,3,2,1] ]]  
      //   [89, [[-180, 4], [-179, 4], ... [180,2] ]]
      // ]
      for(const auto& latvect : cov) {
        auto jslatvect = nlohmann::json::array();
        auto jslongvect = nlohmann::json::array();
        for(const auto& longpair : latvect.second) {
          auto jsdatum = nlohmann::json::array();
          jsdatum.push_back((int)get<0>(longpair));
          jsdatum.push_back(get<1>(longpair));
          jsdatum.push_back(get<2>(longpair));
          jsdatum.push_back(get<3>(longpair));
          jsdatum.push_back((int)(10*get<4>(longpair)));
          jsdatum.push_back((int)(10*get<5>(longpair)));
          jsdatum.push_back((int)(10*get<6>(longpair)));

          jsdatum.push_back((int)(10*get<7>(longpair)));
          jsdatum.push_back((int)(10*get<8>(longpair)));
          jsdatum.push_back((int)(10*get<9>(longpair)));

          jsdatum.push_back((int)(10*get<10>(longpair)));
          jsdatum.push_back((int)(10*get<11>(longpair)));
          jsdatum.push_back((int)(10*get<12>(longpair)));
          jslongvect.push_back(jsdatum);
        }
        jslatvect.push_back(latvect.first);
        jslatvect.push_back(jslongvect);
        
        ret.push_back(jslatvect);
      }
      return ret;
    });

  h2s.addHandler("/sbas.json", [](auto handler, auto req) {
      addHeaders(req);
      auto svstats = g_statskeeper.get();
      auto sbas = g_sbaskeeper.get();
      nlohmann::json ret = nlohmann::json::object();
      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, 
                     NULL, H2O_STRLIT("max-age=3"));

      // Access-Control-Allow-Origin
      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_ACCESS_CONTROL_ALLOW_ORIGIN, 
                     NULL, H2O_STRLIT("*"));

      
      for(const auto& s: sbas) {
        nlohmann::json item  = nlohmann::json::object();
        item["last-seen"] = s.second.status.d_lastSeen;
        item["last-seen-s"] = time(0) - s.second.status.d_lastSeen;

        item["last-type-0"] = s.second.status.d_lastDNU;
        item["last-type-0-s"] = time(0) - s.second.status.d_lastDNU;

        // this 59 is to make sure galmonmon doesn't trigger on a single message
        if(s.second.status.d_lastSeen - s.second.status.d_lastDNU < 59)
          item["health"]="DON'T USE";
        else
          item["health"]="OK";
        
        nlohmann::json perrecv  = nlohmann::json::object();
        for(const auto& recv : s.second.perrecv) {
          perrecv[to_string(recv.first)]["last-seen"] = recv.second.last_seen;
          perrecv[to_string(recv.first)]["last-seen-s"] = time(0) - recv.second.last_seen;
        }
        item["perrecv"]=perrecv;
        
        ret[to_string(s.first)]=item;
      }
      return ret;
    });
  
  h2s.addHandler("/svs.json", [](auto handler, auto req) {
      addHeaders(req);
      auto svstats = g_statskeeper.get();
      nlohmann::json ret = nlohmann::json::object();
      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, 
                     NULL, H2O_STRLIT("max-age=3"));

      // Access-Control-Allow-Origin
      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_ACCESS_CONTROL_ALLOW_ORIGIN, 
                     NULL, H2O_STRLIT("*"));

      
      for(const auto& s: svstats) {
        nlohmann::json item  = nlohmann::json::object();
        if(!s.second.tow()) // I know, I know, will suck briefly
          continue;

        item["gnssid"] = s.first.gnss;
        item["svid"] = s.first.sv;
        item["sigid"] = s.first.sigid;
        // perhaps check ephBeidoumsg for sow >=0 as 'completeIOD'?

        if(s.first.gnss == 3) { // beidou
          item["sisa"]=humanUra(s.second.ephBeidoumsg.urai);
          item["sisa-m"]=numUra(s.second.ephBeidoumsg.urai);
          if(s.second.completeIOD()) {
            item["eph-age-m"] = ephAge(s.second.tow(), s.second.ephBeidoumsg.getT0e())/60.0;

          }

          Point p;
          if(s.second.ephBeidoumsg.sqrtA != 0) {
            getCoordinates(s.second.tow(), s.second.ephBeidoumsg, &p);
            auto beidoualma = g_beidoualmakeeper.get();
            if(auto iter = beidoualma.find(s.first.sv); iter != beidoualma.end()) {
              Point almapos;
              getCoordinates(s.second.tow(), iter->second.alma, &almapos);
              item["alma-dist"] = truncPrec(Vector(almapos, p).length()/1000.0, 1);
            }
          }
        }
        else if(s.first.gnss == 6) { // glonass
          if(s.second.glonassMessage.FT < 16) {
            item["sisa"] = humanFt(s.second.glonassMessage.FT);
            item["sisa-m"] = numFt(s.second.glonassMessage.FT);
          }
          item["aode"] = s.second.ephglomsg.En*24;
          item["iod"] = s.second.ephglomsg.Tb;

          time_t glonow =  nanoTime(6, s.second.wn(), s.second.tow())/1000000000.0;
          // the 820368000 stuff is to rebase to 'day 1' so the % works
          auto pseudoTow = (getGlonassT0e(glonow, s.second.glonassMessage.Tb) - 820368000) % (7*86400);
          
          //          cout<<std::fixed<<"wn: "<<s.second.wn() <<" tow "<<s.second.tow() <<" " << (int) s.second.glonassMessage.Tb << " " << glonow <<" " << humanTime(glonow) <<" ";
          //          cout<< pseudoTow <<" " <<  ephAge(s.second.tow(), getGlonassT0e(pseudoTow, s.second.glonassMessage.Tb))/60.0<<endl;
          item["eph-age-m"] = ephAge(s.second.tow(), getGlonassT0e(pseudoTow, s.second.glonassMessage.Tb))/60.0;

          // this should actually use local time!
          Point p;
          s.second.getCoordinates(s.second.tow(), &p);
          auto match = g_tles.getBestMatch(nanoTime(s.first.gnss, s.second.wn(), s.second.tow())/1000000000.0,
                                           p.x, p.y, p.z);

          item["best-tle"] = match.name;
          item["best-tle-norad"] = match.norad;
          item["best-tle-int-desig"] = match.internat;
          item["best-tle-dist"] = match.distance/1000.0;
        }
        if(s.second.completeIOD()) {
          item["iod"]=s.second.liveIOD().getIOD();
          if(s.first.gnss == 0 || s.first.gnss == 3) {
            item["sisa"]=humanUra(s.second.gpsmsg.ura);
            item["sisa-m"]=truncPrec(numUra(s.second.gpsmsg.ura),3);
            //            cout<<"Asked to convert "<<s.second.ura<<" for sv "<<s.first.first<<","<<s.first.second<<endl;
          }
          else {
            item["sisa"]=humanSisa(s.second.galmsg.sisa);
            item["sisa-m"]=numSisa(s.second.galmsg.sisa);
          }
          item["eph-age-m"] = ephAge(s.second.tow(), s.second.liveIOD().getT0e())/60;
          // CONVERSION
          item["af0"] = 0; // s.second.liveIOD().af0;
          item["af1"] = 0; //s.second.liveIOD().af1;
          item["af2"] = 0; // (int)s.second.liveIOD().af2;
          // XXX conversion
          //          item["t0c"] = s.second.liveIOD().getT0c();
          

          if(s.second.rtcmEphDelta.id.gnss == s.first.gnss && s.second.rtcmEphDelta.id.sv == s.first.sv && s.second.rtcmEphDelta.iod == s.second.liveIOD().getIOD()
             && abs(s.second.rtcmEphDelta.sow - s.second.tow())<60) {
            const auto& ed = s.second.rtcmEphDelta;
            item["rtcm-eph-delta-cm"] = truncPrec(sqrt(ed.radial*ed.radial + ed.along*ed.along + ed.cross*ed.cross)/10.0, 2);
            item["rtcm-eph-radial-cm"] = truncPrec(ed.radial/10.0, 2);
            item["rtcm-eph-along-cm"] = truncPrec(ed.along/10.0, 2);
            item["rtcm-eph-cross-cm"] = truncPrec(ed.cross/10.0, 2);
            item["rtcm-eph-dradial-cm"] = truncPrec(ed.dradial/10.0, 2);
            item["rtcm-eph-dalong-cm"] = truncPrec(ed.dalong/10.0, 2);
            item["rtcm-eph-dcross-cm"] = truncPrec(ed.dcross/10.0, 2);

            const auto& cd = s.second.rtcmClockDelta;
            // so.. the clock delta message has no iod. It relates to "the previous ephemeris message" it seems
            if(cd.id.gnss == s.first.gnss && cd.id.sv == s.first.sv && abs(s.second.rtcmClockDelta.sow - s.second.tow())<60) {
              auto corrclock = cd.dclock0 - 3e8*ldexp(s.second.galmsg.BGDE1E5a - s.second.galmsg.BGDE1E5b, -32);
              item["rtcm-clock-dclock0"] = truncPrec(corrclock * 100, 2); // we turn this into centimeters
              item["rtcm-clock-dclock1"] = truncPrec(cd.dclock1 * 100, 2); // we turn this into centimeters
              item["rtcm-clock-dclock2"] = truncPrec(cd.dclock2 * 100, 2); // we turn this into centimeters
            }
            
          }
          
          Point p;
          Point core;
          
          // this should actually use local time!
          s.second.getCoordinates(s.second.tow(), &p);
          auto match = g_tles.getBestMatch(nanoTime(s.first.gnss, s.second.wn(), s.second.tow())/1000000000.0,
                                           p.x, p.y, p.z);

          item["best-tle"] = match.name;
          item["best-tle-norad"] = match.norad;
          item["best-tle-int-desig"] = match.internat;
          item["best-tle-dist"] = match.distance/1000.0;

          
          item["x"]=p.x;
          item["y"]=p.y;
          item["z"]=p.z;

          if(s.first.gnss == 0) {
            auto gpsalma = g_gpsalmakeeper.get();
            if(auto iter = gpsalma.find(s.first.sv); iter != gpsalma.end()) {
              Point almapos;
              getCoordinates(s.second.tow(), iter->second, &almapos);
              item["alma-dist"] = Vector(almapos, p).length()/1000.0;
            }
          }
          if(s.first.gnss == 2) {
            if(s.second.osnmaTime >= 0 && ephAge(s.second.galmsg.tow, s.second.osnmaTime) < 60)
              item["osnma"] = s.second.osnma;

            if(s.second.impinavTime >= 0 && ephAge(s.second.galmsg.tow, s.second.impinavTime) < 60)
              item["impinav"] = s.second.impinav;

            auto galileoalma = g_galileoalmakeeper.get();
            if(auto iter = galileoalma.find(s.first.sv); iter != galileoalma.end()) {
              Point almapos;
              getCoordinates(s.second.tow(), iter->second, &almapos);
              item["alma-dist"] = Vector(almapos, p).length()/1000.0;
            }
          }
          
          
        }
        // XX conversion
        /*
        item["a0"]=s.second.a0;
        item["a1"]=s.second.a1;
        */
        if(s.first.gnss == 0) { // GPS
          auto deltaUTC = getGPSUTCOffset(s.second.tow(), s.second.wn(), s.second.gpsmsg);
          item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400);

          // CONVERSION
          //          item["t0t"] = s.second.gpsmsg.t0t;
          // item["wn0t"] = s.second.gpsmsg.wn0t;
        }
        if(s.first.gnss == 2) {
          auto deltaUTC = s.second.galmsg.getUTCOffset(s.second.tow(), s.second.wn());
          item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400);
          auto deltaGPS = s.second.galmsg.getGPSOffset(s.second.tow(), s.second.wn());
          item["delta-gps"] = fmt::sprintf("%.1f %+.1f/d", deltaGPS.first, deltaGPS.second * 86400);
          item["t0t"] = s.second.galmsg.t0t;
          item["wn0t"] = s.second.galmsg.wn0t;
        }
        if(s.first.gnss == 3) {
          auto deltaUTC = s.second.ephBeidoumsg.getUTCOffset(s.second.ephBeidoumsg.sow);
          item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400);

          auto deltaGPS = s.second.ephBeidoumsg.getGPSOffset(s.second.ephBeidoumsg.sow);
          item["delta-gps"] = fmt::sprintf("%.1f %+.1f/d", deltaGPS.first, deltaGPS.second * 86400);
          item["t0g"] =0;
          item["wn0g"] = 0;
          
          item["t0t"] = 0;
          item["wn0t"] = 0;
        }


        // XXX conversion
        //        item["dtLS"]=s.second.dtLS;

        if(s.first.gnss == 3) {  // beidou
          item["a0g"]=s.second.ephBeidoumsg.a0gps;
          item["a1g"]=s.second.ephBeidoumsg.a1gps;
          if(s.second.ephBeidoumsg.aode >= 0)
            item["aode"]=s.second.ephBeidoumsg.aode;
          if(s.second.ephBeidoumsg.aodc >= 0)
            item["aodc"]=s.second.ephBeidoumsg.aodc;

          item["health"] = s.second.beidoumsg.sath1 ? "NOT OK" : "OK";
          item["healthissue"] = !!s.second.beidoumsg.sath1;
        }

        if(s.first.gnss == 6) { // GLONASS
          auto deltaUTC = s.second.glonassMessage.getUTCOffset(0);
          item["delta-utc"] = fmt::sprintf("%.1f %+.1f/d", deltaUTC.first, deltaUTC.second * 86400);

          auto deltaGPS = s.second.glonassMessage.getGPSOffset(0);
          item["delta-gps"] = fmt::sprintf("%.1f %+.1f/d", deltaGPS.first, deltaGPS.second * 86400);
        }
        
        if(s.first.gnss == 2) {  // galileo
          item["a0g"]=s.second.galmsg.a0g;
          item["a1g"]=s.second.galmsg.a1g;
          item["t0g"]=s.second.galmsg.t0g;
          item["wn0g"]=s.second.galmsg.wn0g;

          item["health"] =
            humanBhs(s.second.galmsg.e1bhs)                       +"/" +
            humanBhs(s.second.galmsg.e5bhs)                      +"/" +
            (s.second.galmsg.e1bdvs ? "NG" : "val") +"/"+
            (s.second.galmsg.e5bdvs ? "NG" : "val");
          item["e5bdvs"]=s.second.galmsg.e5bdvs;
          item["e1bdvs"]=s.second.galmsg.e1bdvs;
          item["e5bhs"]=s.second.galmsg.e5bhs;
          item["e1bhs"]=s.second.galmsg.e1bhs;
          item["healthissue"]=0;
          if(s.second.galmsg.e1bhs == 2 || s.second.galmsg.e5bhs == 2)
            item["healthissue"] = 1;
          if(s.second.galmsg.e1bhs == 3 || s.second.galmsg.e5bhs == 3)
            item["healthissue"] = 1;
          if(s.second.galmsg.e1bdvs || s.second.galmsg.e5bdvs || s.second.galmsg.e1bhs == 1 || s.second.galmsg.e5bhs == 1)
            item["healthissue"] = 2;
          
        }
        else if(s.first.gnss == 0) {
          item["health"] =s.second.gpsmsg.gpshealth ? ("NOT OK: "+to_string(s.second.gpsmsg.gpshealth)) : string("OK");
          item["healthissue"]= 2* !!s.second.gpsmsg.gpshealth;
        }
        else if(s.first.gnss == 6) { // GLONASS
          item["health"]= s.second.glonassMessage.Bn ? ("NOT OK: "+to_string(s.second.glonassMessage.Bn)) : string("OK");
          item["healthissue"]= 2* !!s.second.glonassMessage.Bn;
        }
        
        nlohmann::json perrecv  = nlohmann::json::object();
        for(const auto& pr : s.second.perrecv) {
          if(pr.second.db > 0 || (time(0) - pr.second.t < 1800)) {
            nlohmann::json det  = nlohmann::json::object();
            det["elev"] = pr.second.el;
            Point sat;
                        
            if(s.second.completeIOD())
              s.second.getCoordinates(latestTow(s.first.gnss, svstats), & sat);

            if(sat.x) {
              Point our = g_srcfacts[pr.first].pos;
              det["elev"] = roundf(10.0*getElevationDeg(sat, our))/10.0;
              det["azi"] = roundf(10.0*getAzimuthDeg(sat, our))/10.0;
            }
            else
              det["elev"] = pr.second.el;
            
            det["db"] = pr.second.db;
            det["last-seen-s"] = time(0) - pr.second.t;
            det["prres"] = truncPrec(pr.second.prres, 2);
            det["qi"] = pr.second.qi;
            det["used"] = pr.second.used;

            if(time(0) - pr.second.deltaHzTime < 60) {
              det["delta_hz"] = pr.second.deltaHz;
              auto hzCorrection = getHzCorrection(time(0), pr.first, s.first.gnss, s.first.sigid, svstats);
              if(hzCorrection)
                det["delta_hz_corr"] = pr.second.deltaHz - *hzCorrection;
            }

            
            perrecv[to_string(pr.first)]=det;
          }
        }
        item["perrecv"]=perrecv;

        item["last-seen-s"] = time(0) - nanoTime(s.first.gnss, s.second.wn(), s.second.tow())/1000000000;

        if(s.second.latestDisco >=0) {
          item["orbit-disco"]= truncPrec(s.second.latestDisco, 3);
          item["orbit-disco-age"]= s.second.latestDiscoAge;
        }
        if(s.second.timeDisco > -100 && s.second.timeDisco < 100) {
          item["time-disco"]= truncPrec(s.second.timeDisco, 1);
        }

        
        item["wn"] = s.second.wn();
        item["tow"] = s.second.tow();
        item["fullName"] = makeSatIDName(s.first);
        item["name"] = makeSatPartialName(s.first);
        ret[makeSatIDName(s.first)] = item;
      }
      return ret;
    });

  h2s.addHandler("/sbstatus.json", [](auto handler, auto req) {
      addHeaders(req);
      auto svstats = g_statskeeper.get();
      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, 
                     NULL, H2O_STRLIT("max-age=3"));

      // Access-Control-Allow-Origin
      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_ACCESS_CONTROL_ALLOW_ORIGIN, 
                     NULL, H2O_STRLIT("*"));

      auto ret = nlohmann::json::array();

      time_t now = time(0);
      for(const auto& s: svstats) {
        for(const auto& sb : s.second.sbas) {
          if(now - sb.second.longterm.lastUpdate > 120)
            continue;
          if(now - sb.second.fast.lastUpdate > 120)
            continue;
          if(sb.second.fast.udrei == 14)
            continue;

          auto obj = nlohmann::json::object();
          obj["name"] = makeSatPartialName(s.first);
          obj["sbasprn"] = sb.first;

          obj["dx"] = sb.second.longterm.dx;
          obj["dy"] = sb.second.longterm.dy;
          obj["dz"] = sb.second.longterm.dz;
          obj["dai"] = sb.second.longterm.dai;
          obj["iod8"] = sb.second.longterm.iod8;
          obj["velocity"] = sb.second.longterm.velocity;

          obj["last-longterm-update-s"] = now - sb.second.longterm.lastUpdate;
          obj["last-fast-update-s"] = now - sb.second.fast.lastUpdate;

     
          if(sb.second.longterm.velocity) {
            obj["toa"] = sb.second.longterm.toa;
            int toadelta = (now % 86400) - sb.second.longterm.toa;
            
            obj["toa-delta"] = toadelta;

            obj["ddx"] = sb.second.longterm.ddx;
            obj["ddy"] = sb.second.longterm.ddy;
            obj["ddz"] = sb.second.longterm.ddz;
            obj["ddai"] = sb.second.longterm.ddai;

            obj["adx"] = sb.second.longterm.dx + toadelta*sb.second.longterm.ddx;
            obj["ady"] = sb.second.longterm.dy + toadelta*sb.second.longterm.ddy;
            obj["adz"] = sb.second.longterm.dz + toadelta*sb.second.longterm.ddz;
            obj["adai"] = sb.second.longterm.dai + toadelta*sb.second.longterm.ddai;

            
          }
          obj["correction"] = sb.second.fast.correction;
          obj["udrei"] = sb.second.fast.udrei;

          if(s.second.completeIOD() && (s.second.liveIOD().getIOD() & 0xff) == sb.second.longterm.iod8) {
            Point sat;
            
            s.second.getCoordinates(s.second.tow(), &sat);
            Point adjsat=sat;
            adjsat.x += sb.second.longterm.dx;
            adjsat.y += sb.second.longterm.dy;
            adjsat.z += sb.second.longterm.dz;
            Point sbasCenter;
            int prn = sb.first;
            if(prn== 126 || prn == 136 || prn == 123)
              sbasCenter = c_egnosCenter;
            else if(prn == 138 || prn == 131 || prn == 133)
              sbasCenter = c_waasCenter;
            else
              sbasCenter = Point{0,0,0};
            
            double dist = Vector(sbasCenter, adjsat).length() - Vector(sbasCenter, sat).length();
            obj["space-shift"] = dist;
            dist -= sb.second.longterm.dai / 3;
            obj["eph-shift"] = dist;
            obj["range-shift"] = dist - sb.second.fast.correction;
          }

          
          ret.push_back(obj);
        }
      }
      return ret;
    });

  
  h2s.addDirectory("/", htmlDir);

  const char *address = localAddress.c_str();
  std::thread ws([&h2s, address]() {
      auto actx = h2s.addContext();
      ComboAddress listenOn(address);
      h2s.addListener(listenOn, actx);
      cout<<"Listening on "<< listenOn.toStringWithPort() <<endl;
      h2s.runLoop();
    });
  ws.detach();

  try {
  for(;;) {
    static time_t lastWebSync;
    if(lastWebSync != time(0)) {
      g_statskeeper.set(g_svstats);
      g_sbaskeeper.set(g_sbas);
      g_galileoalmakeeper.set(g_galileoalma);
      g_glonassalmakeeper.set(g_glonassalma);
      g_beidoualmakeeper.set(g_beidoualma);
      g_gpsalmakeeper.set(g_gpsalma);
      lastWebSync = time(0);
    }

    char bert[6];
    // I apologise deeply
    if(fread(bert, 1, sizeof(bert), stdin) != sizeof(bert) || bert[0]!='b' || bert[1]!='e' || bert[2] !='r' || bert[3]!='t') {
      cerr<<"EOF or bad magic"<<endl;
      break;
    }
    
    uint16_t len;
    memcpy(&len, bert+4, 2);
    len = htons(len);
    char buffer[len];
    if(fread(buffer, 1, len, stdin) != len)
      break;

    
    NavMonMessage nmm;
    try {
      if(!nmm.ParseFromString(string(buffer, len))) {
        cerr<<"Parsing error nmm"<<endl;
        continue;
      }
        
    }
    catch(std::exception& e) {
      cerr<<"nmm exception: "<<e.what();
      continue;
    }
    catch(...) {
      cerr<<"nmm exception"<<endl;
      continue;
    }
      
    constexpr auto lastCovInterval = 300;
    static time_t lastCovSyncPoint;
    static time_t holdOffTime;
    if(nmm.localutcseconds() / lastCovInterval > (unsigned int)lastCovSyncPoint) {
      if(!lastCovSyncPoint)
        holdOffTime = nmm.localutcseconds() + 600;
      
      if((time_t)nmm.localutcseconds() > holdOffTime)
        storeCoverageStats(idb, nmm.localutcseconds());
      lastCovSyncPoint = nmm.localutcseconds() / lastCovInterval;
    }
   
    constexpr auto lastSelfstatInterval = 60;
    static time_t lastSelfstatSyncPoint;
    if(nmm.localutcseconds() / lastSelfstatInterval > (unsigned int)lastSelfstatSyncPoint) {
      storeSelfStats(idb, nmm.localutcseconds());
      lastSelfstatSyncPoint = nmm.localutcseconds() / lastSelfstatInterval;
    }

    #if 0
    constexpr auto lastIonoInterval = 3600;
    static time_t lastIonoSyncPoint;
    if(nmm.localutcseconds() / lastIonoInterval > (unsigned int)lastIonoSyncPoint) {
      // go over all satellites
      NeQuickInst nqi;
      //      cerr<<"Looking at all sats"<<endl;
      for(const auto& s : g_svstats) {
        if(s.first.gnss!=2 || !s.second.completeIOD())
          continue;
        Point sat;
        s.second.getCoordinates(s.second.tow(), &sat);
        for(const auto& pr : s.second.perrecv) {
          //          cerr<<"Looking at "<<s.first.sv<<", "<<pr.second.db<<" "<<nmm.localutcseconds()<<", "<<pr.second.t<<" perrecv "<<pr.first<<" count " << g_srcfacts.count(pr.first)<<endl;
          if(g_srcfacts.count(pr.first) && (pr.second.db > 0 || (nmm.localutcseconds() - pr.second.t < 120))) {
            //            cerr<<"Doing it -> "<< s.second.galmsg.ai0 <<" " <<s.second.galmsg.ai1<<" " << s.second.galmsg.ai2<< endl;
            const auto& sp = g_srcfacts[pr.first];
            try {
              //              cerr<<"Obs "<<pr.first<<" pos: "<<sp.pos.x<<", "<<sp.pos.y<<", "<<sp.pos.z<<endl;
              //              cerr<<"Sat " <<s.first.sv<<" pos: "<<sat.x<<", "<<sat.y<<", "<<sat.z<<endl;
              //              auto obs = ecefToWGS84Deg(sp.pos.x, sp.pos.y, sp.pos.z);
              //              cerr<<"Observer height: "<<get<2>(obs)<<" meters, long "<<get<0>(obs)<<", lat "<<get<1>(obs)<<endl;

              //              auto satdegs = ecefToWGS84Deg(sat.x, sat.y, sat.z);
              //              cerr<<"Satellite height: "<<get<2>(satdegs)<<" meters, long "<<get<0>(satdegs)<<", lat "<<get<1>(satdegs)<< " -> elevation "<<getElevationDeg(sat, sp.pos)<<" deg"<<endl;

              if(getElevationDeg(sat, sp.pos) < 0) // below the horizon
                continue;
              
              if(sp.pos.x==0 || isnan(sat.x)) {
                //                cerr<<"Fake position, skipping"<<endl;
                continue;
              }
              double meters = (40.3e16/(1575420000.0*1575420000.0)) *
                nqi.getTecu(nmm.localutcseconds(),
                            s.second.galmsg.ai0,
                            s.second.galmsg.ai1,
                            s.second.galmsg.ai2, sp.pos, sat);
              
              idb.addValue(s.first, "nequick",
                           {
                           {"meters", meters}
                           }, nmm.localutcseconds() + nmm.localutcnanoseconds()/1000000000.0, pr.first);
              //              cerr<<"Meters: "<<meters<<endl;
            }
            catch(std::exception& e) {
              cerr<<"Exception during NeQuick: "<<e.what()<<endl;
            }

          }
        }
      }
      //      cerr<<"Done"<<endl;
      lastIonoSyncPoint = nmm.localutcseconds() / lastIonoInterval;
    }
#endif
    
    
    if(nmm.type() == NavMonMessage::ReceptionDataType) {
      int gnssid = nmm.rd().gnssid();
      int sv = nmm.rd().gnsssv();
      int sigid = nmm.rd().sigid();
      if(gnssid==2 && sigid == 0)
        sigid = 1;
      
      SatID id{(uint32_t)gnssid, (uint32_t)sv, (uint32_t)sigid};
      auto& perrecv = g_svstats[id].perrecv[nmm.sourceid()];

      perrecv.db = nmm.rd().db();
      
      perrecv.el = nmm.rd().el();
      perrecv.azi = nmm.rd().azi();

      perrecv.prres = nmm.rd().prres();
      if(nmm.rd().has_qi())
        perrecv.qi = nmm.rd().qi();
      else
        perrecv.qi = -1;

      if(nmm.rd().has_used())
        perrecv.used = nmm.rd().used();
      else
        perrecv.used = -1;
      
      Point sat{0,0,0};
      //cout<<"Got recdata for "<<id.gnss<<","<<id.sv<<","<<id.sigid<<": count="<<g_svstats.count(id)<<endl;
      if(g_svstats[id].completeIOD() && !(random() % 16)) { 
        g_svstats[id].getCoordinates(g_svstats[id].tow(), &sat);
      }
      
      if(sat.x != 0 && g_srcfacts[nmm.sourceid()].pos.x != 0) {
        if(doLogRFData && !(random() % 16))
        idb.addValue(id, "recdata",
                     {
                     {"db", nmm.rd().db()},
                       {"azi", getAzimuthDeg(sat, g_srcfacts[nmm.sourceid()].pos)},
                         {"ele", getElevationDeg(sat, g_srcfacts[nmm.sourceid()].pos)},
                           {"prres", nmm.rd().prres()},
                             {"qi", perrecv.qi},
                               {"used", perrecv.used}
                     }, nmm.localutcseconds() + nmm.localutcnanoseconds()/1000000000.0, nmm.sourceid());
      }

    }
    else if(nmm.type() == NavMonMessage::GalileoInavType) {
      basic_string<uint8_t> inav((uint8_t*)nmm.gi().contents().c_str(), nmm.gi().contents().size());
      int sv = nmm.gi().gnsssv();
      int sigid;
      if(nmm.gi().has_sigid())
        sigid = nmm.gi().sigid();
      else
        sigid = 1;  // default to E1B
      SatID id={2,(uint32_t)sv,(uint32_t)sigid};
      
      auto& svstat = g_svstats[id];
      svstat.gnss = id.gnss;
      auto oldgm = svstat.galmsg;

      auto& gm = svstat.galmsg;
      unsigned int wtype = gm.parse(inav);

      auto& o = g_srcfacts[nmm.sourceid()];
      if(nmm.gi().has_ssp()) {
        o.impinav = true;
        o.impinavTime = nmm.localutcseconds();
      }
      
      if(wtype == 5 && svstat.galmsgTyped.count(5)) {
        const auto& old5gm = svstat.galmsgTyped[5];
        if(make_tuple(old5gm.e5bhs, old5gm.e1bhs, old5gm.e5bdvs, old5gm.e1bdvs) !=
           make_tuple(gm.e5bhs, gm.e1bhs, gm.e5bdvs, gm.e1bdvs)) {
          cout<<humanTime(id.gnss, svstat.wn(), svstat.tow())<<" src "<<nmm.sourceid()<<" Galileo "<<id.sv <<" sigid "<<id.sigid<<" change in health: ["<<humanBhs(old5gm.e5bhs)<<", "<<humanBhs(old5gm.e1bhs)<<", "<<(int)old5gm.e5bdvs <<", " << (int)old5gm.e1bdvs<<"] -> ["<< humanBhs(gm.e5bhs)<<", "<< humanBhs(gm.e1bhs)<<", "<< (int)gm.e5bdvs <<", " << (int)gm.e1bdvs<<"], lastseen "<<ephAge(old5gm.tow, gm.tow)/3600.0 <<" hours"<<endl;
        }
      }
      
      svstat.galmsgTyped[wtype] = gm;

      if(wtype == 1 || wtype == 2 || wtype == 3 || wtype == 4) {
        if(nmm.gi().has_reserved1()) {
          static string off;
          if(off.empty())
            off.append(5, (char)0);
          svstat.osnma = nmm.gi().reserved1() != off;
          if(svstat.osnma) // eventually this will become too much but ok for now
            idb.addValue(id, "osnma", {{"wtype", wtype}, {"field", makeHexDump(nmm.gi().reserved1())}}, satUTCTime(id));
          svstat.osnmaTime = gm.tow;
        }
        idb.addValue(id, "ephemeris", {{"iod-live", svstat.galmsg.iodnav},
              {"eph-age", ephAge(gm.tow, gm.getT0e())}}, satUTCTime(id));

        int w = 1;
        for(; w < 5; ++w) {
          if(!svstat.galmsgTyped.count(w))
            break;
          if(w > 1 && svstat.galmsgTyped[w-1].iodnav != svstat.galmsgTyped[w].iodnav)
            break;
        }
        if(w==5) { // have complete new ephemeris
          if(svstat.ephgalmsg.iodnav != svstat.galmsgTyped[1].iodnav) {
            svstat.oldephgalmsg = svstat.ephgalmsg;
            svstat.ephgalmsg = svstat.galmsgTyped[wtype];
            svstat.reportNewEphemeris(id, idb);
          }
        }
      }

      svstat.perrecv[nmm.sourceid()].t = nmm.localutcseconds();

      if(wtype >=1 && wtype <= 4) { // ephemeris 
        if(wtype == 3) {
          idb.addValue(id, "sisa", {{"value", svstat.galmsg.sisa}}, satUTCTime(id));
        }
        else if(wtype == 4) {
          idb.addValue(id, "clock", {{"offset_ns", svstat.galmsg.getAtomicOffset(svstat.tow()).first}, 
                  {"t0c", svstat.galmsg.t0c*60}, // getT0c()??
                {"af0", svstat.galmsg.af0},
                  {"af1", svstat.galmsg.af1},
                    {"af2", svstat.galmsg.af2}}, satUTCTime(id)); 


            if(oldgm.af0 && oldgm.t0c != svstat.galmsg.t0c) {
              auto oldOffset = oldgm.getAtomicOffset(svstat.tow());
              auto newOffset = svstat.galmsg.getAtomicOffset(svstat.tow());
              svstat.timeDisco = oldOffset.first - newOffset.first;
              if(fabs(svstat.timeDisco) < 10000)
                idb.addValue(id, "clock_jump_ns", {
                    {"jump", svstat.timeDisco},
                    {"duration", ephAge(svstat.galmsg.t0c * 60, oldgm.t0c * 60)},
                    {"old-af0", oldgm.af0},
                    {"old-af1", oldgm.af1},
                    {"old-af2", oldgm.af2},
                    {"old-t0c", oldgm.t0c * 60},
                    {"new-af0", svstat.galmsg.af0},
                    {"new-af1", svstat.galmsg.af1},
                    {"new-af2", svstat.galmsg.af2},
                    {"new-t0c", svstat.galmsg.t0c * 60}

                  }, satUTCTime(id));
            }
          }
        }
        else if(wtype == 5) {
          idb.addValue(id, "iono", {
              {"ai0", svstat.galmsg.ai0},
                {"ai1", svstat.galmsg.ai1},
                  {"ai2", svstat.galmsg.ai2},
                    {"sf1", svstat.galmsg.sf1},
                      {"sf2", svstat.galmsg.sf2},
                        {"sf3", svstat.galmsg.sf3},
                          {"sf4", svstat.galmsg.sf4},
                            {"sf5", svstat.galmsg.sf5}}, satUTCTime(id));

          
          idb.addValue(id, "galbgd", {
              {"BGDE1E5a", svstat.galmsg.BGDE1E5a},
                {"BGDE1E5b", svstat.galmsg.BGDE1E5b}}, satUTCTime(id));

          
          idb.addValue(id, "galhealth", {
              {"e1bhs", svstat.galmsg.e1bhs}, 
                {"e5bhs", svstat.galmsg.e5bhs},
                  {"e5bdvs", svstat.galmsg.e5bdvs},
                    {"e1bdvs", svstat.galmsg.e1bdvs}}, satUTCTime(id));
        }
        else if(wtype == 6) {  // GST-UTC
          const auto& sv = svstat;
          g_GSTUTCOffset = sv.galmsg.getUTCOffset(sv.tow(), sv.wn()).first;
          idb.addValue(id, "utcoffset", {
              {"a0", sv.galmsg.a0},
                {"a1", sv.galmsg.a1},
                  {"t0t", sv.galmsg.t0t},
                    {"delta", g_GSTUTCOffset}
            },
            satUTCTime(id));
          
          g_dtLS = sv.galmsg.dtLS;
        }
        else if(wtype == 7) {
          // this contains first part of alma1
        }
        else if(wtype == 8) {
          if(gm.tow - svstat.galmsgTyped[7].tow < 5 &&
             svstat.galmsgTyped[7].alma1.svid && gm.iodalmanac == svstat.galmsgTyped[7].iodalmanac) {
            //            cout<<(int)wtype<<" alma-sv "<<gm.alma1.svid<< " "<<gm.alma1.deltaSqrtA<<" " <<gm.alma1.t0almanac << endl;
            g_galileoalma[gm.alma1.svid] = gm.alma1;
          }
          
        }
        else if(wtype == 9) {          
          if(gm.tow - svstat.galmsgTyped[8].tow <= 30 &&
             svstat.galmsgTyped[8].alma2.svid && gm.iodalmanac == svstat.galmsgTyped[8].iodalmanac) {
            //            cout<<(int)wtype<<" alma-sv "<<gm.alma2.svid<< " "<<gm.alma2.deltaSqrtA<<" " <<gm.alma2.t0almanac << endl;
            g_galileoalma[gm.alma2.svid] = gm.alma2;
          }
        }

        else if(wtype == 10) { // GSTT GPS
          if(gm.tow - svstat.galmsgTyped[9].tow < 5 &&
             svstat.galmsgTyped[9].alma3.svid && gm.iodalmanac == svstat.galmsgTyped[9].iodalmanac) {
            //            cout<<(int)wtype<<" alma-sv "<<gm.alma3.svid<< " "<<gm.alma3.deltaSqrtA<<" " <<gm.alma3.t0almanac << endl;
            //            cout<<(int)wtype<<"b alma-sv "<<svstat.galmsgTyped[9].alma3.svid<< " "<<svstat.galmsgTyped[9].alma3.deltaSqrtA<<" " <<svstat.galmsgTyped[9].alma3.t0almanac << endl;
            g_galileoalma[gm.alma3.svid] = gm.alma3;
          }
          g_GSTGPSOffset = gm.getGPSOffset(gm.tow, gm.wn).first;
          idb.addValue(id, "gpsoffset", {{"a0g", svstat.galmsg.a0g},
                {"a1g", svstat.galmsg.a1g},
                  {"t0g", svstat.galmsg.t0g},
                    {"wn0g", svstat.galmsg.wn0g},
                      {"delta", g_GSTGPSOffset}
            }, satUTCTime(id));
          
        }
        else if(wtype == 16) { // redced
          idb.addValue(id, "redced", {{"deltaared", svstat.galmsg.deltaAred},
                                      {"exred", svstat.galmsg.a1g},
                                      {"eyred", svstat.galmsg.t0g},
                                      {"deltai0red", svstat.galmsg.deltai0red},
                                      {"omega0red", svstat.galmsg.omega0red},
                                      {"lambda0red", svstat.galmsg.lambda0red},
                                      {"af0red", svstat.galmsg.af0red},
                                      {"af1red", svstat.galmsg.af1red},
                                      {"t0r",  1 + nmm.gi().gnsstow()  - (nmm.gi().gnsstow() % 30)},
            }, satUTCTime(id));
          svstat.impinav = true;
          svstat.impinavTime = nmm.gi().gnsstow();
        }
    }
    else if(nmm.type() == NavMonMessage::GalileoCnavType) {
      SatID id={2,(uint32_t) nmm.gc().gnsssv(), 8}; // E6
      idb.addValue(id, "galcnav", {{"msg", makeHexDump(nmm.gc().contents())}},
                   nmm.localutcseconds());
      // ... no idea what this contains
    }
    else if(nmm.type() == NavMonMessage::GalileoFnavType) {
      basic_string<uint8_t> fnav((uint8_t*)nmm.gf().contents().c_str(), nmm.gf().contents().size());
      int sv = nmm.gf().gnsssv();
      SatID id={2,(uint32_t)sv,6}; // E5a
      
      auto& svstat = g_svstats[id];
      svstat.gnss = id.gnss;
      auto oldgm = svstat.galmsg;

      auto& gm = svstat.galmsg;
      unsigned int wtype = gm.parseFnav(fnav);

      
      if(wtype == 1 && svstat.galmsgTyped.count(1)) {
        const auto& old5gm = svstat.galmsgTyped[1];
        if(make_tuple(old5gm.e5ahs, old5gm.e1bhs, old5gm.e5advs, old5gm.e1bdvs) !=
           make_tuple(gm.e5ahs, gm.e1bhs, gm.e5advs, gm.e1bdvs)) {
          cout<<humanTime(id.gnss, svstat.wn(), svstat.tow())<<" src "<<nmm.sourceid()<<" Galileo "<<id.sv <<" sigid "<<id.sigid<<" change in health: ["<<humanBhs(old5gm.e5ahs)<<", "<<humanBhs(old5gm.e1bhs)<<", "<<(int)old5gm.e5advs <<", " << (int)old5gm.e1bdvs<<"] -> ["<< humanBhs(gm.e5ahs)<<", "<< humanBhs(gm.e1bhs)<<", "<< (int)gm.e5advs <<", " << (int)gm.e1bdvs<<"], lastseen "<<ephAge(old5gm.tow, gm.tow)/3600.0 <<" hours"<<endl;
        }
      }
      
      svstat.galmsgTyped[wtype] = gm;

      if(wtype == 1 || wtype == 2 || wtype == 3 || wtype == 4) {
        idb.addValue(id, "ephemeris", {{"iod-live", svstat.galmsg.iodnav},
              {"eph-age", ephAge(gm.tow, gm.getT0e())}}, satUTCTime(id));

        int w = 1;
        for(; w < 5; ++w) {
          if(!svstat.galmsgTyped.count(w))
            break;
          if(w > 1 && svstat.galmsgTyped[w-1].iodnav != svstat.galmsgTyped[w].iodnav)
            break;
        }
        if(w==5) { // have complete new ephemeris

          if(svstat.ephgalmsg.iodnav != svstat.galmsgTyped[2].iodnav) {
            //            cout<<"New F/NAV ephemeris for "<<makeSatIDName(id)<<" iod " << svstat.galmsgTyped[1].iodnav << " t0e " << svstat.galmsgTyped[3].t0e << " af0 "<< gm.af0 <<" iod1 "<<svstat.galmsgTyped[1].iodnav;
            /*
            cout<<" iod2 "<<svstat.galmsgTyped[2].iodnav;
            cout<<" iod3 "<<svstat.galmsgTyped[3].iodnav;
            cout<<" iod4 "<<svstat.galmsgTyped[4].iodnav << endl;
            */
            svstat.oldephgalmsg = svstat.ephgalmsg;
            svstat.ephgalmsg = svstat.galmsg;
            svstat.reportNewEphemeris(id, idb);
          }
        }
      }

      svstat.perrecv[nmm.sourceid()].t = nmm.localutcseconds();

      if(wtype >=1 && wtype <= 4) { // ephemeris 
        if(wtype == 1) {
          idb.addValue(id, "sisa", {{"value", svstat.galmsg.sisa}}, satUTCTime(id));

          idb.addValue(id, "galbgd", {
              {"BGDE1E5a", svstat.galmsg.BGDE1E5a},
                }, satUTCTime(id));
          
          
          idb.addValue(id, "galhealth", {
                {"e5ahs", svstat.galmsg.e5bhs},
                  {"e5advs", svstat.galmsg.e5bdvs}
                    }, satUTCTime(id));

          
          idb.addValue(id, "clock", {{"offset_ns", svstat.galmsg.getAtomicOffset(svstat.tow()).first}, 
                  {"t0c", svstat.galmsg.t0c*60}, // getT0c()??
                {"af0", svstat.galmsg.af0},
                  {"af1", svstat.galmsg.af1},
                    {"af2", svstat.galmsg.af2}}, satUTCTime(id)); 
       
          if(oldgm.af0 && oldgm.t0c != svstat.galmsg.t0c) {
            auto oldOffset = oldgm.getAtomicOffset(svstat.tow());
            auto newOffset = svstat.galmsg.getAtomicOffset(svstat.tow());
            svstat.timeDisco = oldOffset.first - newOffset.first;
            if(fabs(svstat.timeDisco) < 10000)
              idb.addValue(id, "clock_jump_ns", {
                  {"jump", svstat.timeDisco},
                    {"duration", ephAge(svstat.galmsg.t0c * 60, oldgm.t0c * 60)},
                    {"old-af0", oldgm.af0},
                    {"old-af1", oldgm.af1},
                    {"old-af2", oldgm.af2},
                    {"old-t0c", oldgm.t0c * 60},
                    {"new-af0", svstat.galmsg.af0},
                    {"new-af1", svstat.galmsg.af1},
                    {"new-af2", svstat.galmsg.af2},
                    {"new-t0c", svstat.galmsg.t0c * 60}

                  }, satUTCTime(id));
          }

          idb.addValue(id, "iono", {
              {"ai0", svstat.galmsg.ai0},
                {"ai1", svstat.galmsg.ai1},
                  {"ai2", svstat.galmsg.ai2},
                    {"sf1", svstat.galmsg.sf1},
                      {"sf2", svstat.galmsg.sf2},
                        {"sf3", svstat.galmsg.sf3},
                          {"sf4", svstat.galmsg.sf4},
                            {"sf5", svstat.galmsg.sf5}}, satUTCTime(id));
          
        }
      }
      else if(wtype == 4) {
        const auto& sv = g_svstats[id];
        g_GSTUTCOffset = sv.galmsg.getUTCOffset(sv.tow(), sv.wn()).first;
        idb.addValue(id, "utcoffset", {
            {"a0", sv.galmsg.a0},
              {"a1", sv.galmsg.a1},
                {"t0t", sv.galmsg.t0t},
                  {"delta", g_GSTUTCOffset}
          },
          satUTCTime(id));
          
        g_dtLS = sv.galmsg.dtLS;
        g_GSTGPSOffset = gm.getGPSOffset(gm.tow, gm.wn).first;
        idb.addValue(id, "gpsoffset", {{"a0g", svstat.galmsg.a0g},
              {"a1g", svstat.galmsg.a1g},
                {"t0g", svstat.galmsg.t0g},
                  {"wn0g", svstat.galmsg.wn0g},
                    {"delta", g_GSTGPSOffset}
          }, satUTCTime(id));
      }
    }
    else if(nmm.type() == NavMonMessage::ObserverPositionType) {
      g_srcfacts[nmm.sourceid()].lastSeen = nmm.localutcseconds();
      g_srcfacts[nmm.sourceid()].pos.x = nmm.op().x();
      g_srcfacts[nmm.sourceid()].pos.y = nmm.op().y();
      g_srcfacts[nmm.sourceid()].pos.z = nmm.op().z();
      if(nmm.op().has_groundspeed()) {
        g_srcfacts[nmm.sourceid()].groundSpeed = nmm.op().groundspeed();
      }
      
      g_srcfacts[nmm.sourceid()].accuracy = nmm.op().acc();
      //      idb.addValueObserver(nmm.sourceid(), "accfix", nmm.op().acc(), nmm.localutcseconds());

      auto latlonh = ecefToWGS84(nmm.op().x(), nmm.op().y(), nmm.op().z());
      idb.addValueObserver(nmm.sourceid(), "fix", {
          {"x", nmm.op().x()},
            {"y", nmm.op().y()},
              {"z", nmm.op().z()},
                {"lat", 180.0*std::get<0>(latlonh)/M_PI},
                    {"lon", 180.0*std::get<1>(latlonh)/M_PI},
                      {"h", std::get<2>(latlonh)},
                        {"acc", nmm.op().acc()},
                          {"groundspeed", nmm.op().has_groundspeed() ? nmm.op().groundspeed() : -1.0}
        }, 
        nmm.localutcseconds());
    }
    else if(nmm.type() == NavMonMessage::RFDataType) {
      SatID id{nmm.rfd().gnssid(), nmm.rfd().gnsssv(), nmm.rfd().sigid()};

      
      if(id.gnss==2 && id.sigid == 0) // old ubxtool output gets this wrong
        id.sigid = 1;

      // some sources ONLY have RFDatatype & not ReceptionDataType
      g_svstats[id].perrecv[nmm.sourceid()].db = nmm.rfd().cno();


      if(doLogRFData)
        idb.addValueObserver(nmm.sourceid(),  "rfdata",
                   {{"carrierphase", nmm.rfd().carrierphase()},
                       {"doppler", nmm.rfd().doppler()},
                         {"locktime", nmm.rfd().locktimems()},
                           {"pseudorange", nmm.rfd().pseudorange()},
                             {"prstd", nmm.rfd().prstd()},
                               {"cpstd", nmm.rfd().cpstd()},
                                 {"dostd", nmm.rfd().dostd()}

                   }, nanoTime(0, nmm.rfd().rcvwn(), nmm.rfd().rcvtow())/1000000000.0, id);
      
      if(id.gnss == 3 && g_svstats[id].ephBeidoumsg.sow >= 0 && g_svstats[id].ephBeidoumsg.sqrtA != 0) {
        double freq = 1561.098;
        if(nmm.rfd().sigid() != 0)
          freq = 1207.140;

        // the magic 14 is because 'rcvtow()' is in GPS/Galileo TOW
        // but BeiDou operates with 14 leap seconds less than GPS/Galileo
        auto res = doDoppler(nmm.rfd().rcvtow()-14, g_srcfacts[nmm.sourceid()].pos, g_svstats[id].ephBeidoumsg, freq * 1000000);

        if(isnan(res.preddop)) {
          cerr<<"Problem with doppler calculation for C"<<id.sv<<": "<<endl;
          exit(1);
        }
        
        time_t t = utcFromGPS(nmm.rfd().rcvwn(), nmm.rfd().rcvtow());
        if(t - g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime > 10) {
          
          g_svstats[id].perrecv[nmm.sourceid()].deltaHz = nmm.rfd().doppler() -  res.preddop;
          g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime = t;
          
          //          idb.addValue(id, "delta_hz", nmm.rfd().doppler() -  res.preddop);
          auto corr = getHzCorrection(t, nmm.sourceid(), id.gnss, id.sigid, g_svstats);
          if(corr) {
            //            idb.addValue(id, "delta_hz_cor", nmm.rfd().doppler() -  res.preddop - (*corr));
            Point sat;
            getCoordinates(nmm.rfd().rcvtow(), g_svstats[id].ephBeidoumsg, &sat);

            if(doLogRFData)
              idb.addValue(id, "correlator",
                   {{"delta_hz_cor", nmm.rfd().doppler() -  res.preddop - (*corr)},
                       {"delta_hz", nmm.rfd().doppler() -  res.preddop},
                         {"elevation", getElevationDeg(sat, g_srcfacts[nmm.sourceid()].pos)},
                           {"hz", nmm.rfd().doppler()},
                           {"prres", g_svstats[id].perrecv[nmm.sourceid()].prres},
                             {"qi", g_svstats[id].perrecv[nmm.sourceid()].qi},
                               {"used", g_svstats[id].perrecv[nmm.sourceid()].used},
                               {"db", g_svstats[id].perrecv[nmm.sourceid()].db}
                   }, nanoTime(0, nmm.rfd().rcvwn(), nmm.rfd().rcvtow())/1000000000.0, nmm.sourceid()); //this time is supplied in GPS timeframe

            
          }
        }
      }
      else if(g_svstats[id].completeIOD() &&
              (id.gnss != 6 || !(random() % 16))) { // GLONASS is too slow
        double freqMHZ =  1575.42;
        if(id.gnss == 2 && id.sigid == 5) // this is exactly the beidou b2i freq?
          freqMHZ = 1207.140;
        
        auto res = g_svstats[id].doDoppler(nmm.rfd().rcvtow(), g_srcfacts[nmm.sourceid()].pos,freqMHZ * 1000000);
        
        Point sat;
        g_svstats[id].getCoordinates(nmm.rfd().rcvtow(),  &sat);
        
        time_t t = utcFromGPS(nmm.rfd().rcvwn(), nmm.rfd().rcvtow());
//        idb.addValueObserver((int)nmm.sourceid(), "orbit",
  //                     {{"preddop", res.preddop},
    //                       {"radvel", res.radvel}}, t, id);



        if(t - g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime > 10) { // only replace after 5 seconds
          g_svstats[id].perrecv[nmm.sourceid()].deltaHz =  nmm.rfd().doppler() -  res.preddop;
          g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime =  t;
          //          idb.addValue(id, "delta_hz", nmm.rfd().doppler() -  res.preddop);
          auto corr = getHzCorrection(t, nmm.sourceid(), id.gnss, id.sigid, g_svstats);
          if(corr) {
            //            idb.addValue(id, "delta_hz_cor", nmm.rfd().doppler() -  res.preddop - *corr, nmm.sourceid());
            if(doLogRFData)
            idb.addValue(id, "correlator",
                         {{"delta_hz_cor", nmm.rfd().doppler() -  res.preddop - (*corr)},
                             {"delta_hz", nmm.rfd().doppler() -  res.preddop},
                               {"hz", nmm.rfd().doppler()},
                                 {"elevation", getElevationDeg(sat, g_srcfacts[nmm.sourceid()].pos)},
                                   {"prres", g_svstats[id].perrecv[nmm.sourceid()].prres},
                                     {"qi", g_svstats[id].perrecv[nmm.sourceid()].qi},
                                       {"used", g_svstats[id].perrecv[nmm.sourceid()].used},
                                         {"db", g_svstats[id].perrecv[nmm.sourceid()].db}
                               
                         }, t, nmm.sourceid());


          }
        }
      }
    }
    else if(nmm.type()== NavMonMessage::ObserverDetailsType) {
      auto& o = g_srcfacts[nmm.sourceid()];
      o.serialno = nmm.od().serialno();

      o.swversion = nmm.od().swversion();
      o.hwversion = nmm.od().hwversion();
      o.mods = nmm.od().modules();
      if(nmm.od().has_clockoffsetns())
        o.clockOffsetNS = nmm.od().clockoffsetns();
      else
        o.clockOffsetNS = -1;
      
      if(nmm.od().has_clockoffsetdriftns())
        o.clockOffsetDriftNS = nmm.od().clockoffsetdriftns();
      else
        o.clockOffsetDriftNS = -1;
      
      if(nmm.od().has_clockaccuracyns())
        o.clockAccuracyNS = nmm.od().clockaccuracyns();
      else
        o.clockAccuracyNS = -1;
      
      if(nmm.od().has_freqaccuracyps())
        o.freqAccuracyPS = nmm.od().freqaccuracyps();
      else
        o.freqAccuracyPS = -1;

      if(nmm.od().has_uptime())
        o.uptime = nmm.od().uptime();
      else
        o.uptime = -1;

      if(nmm.od().has_recvgithash())
        o.githash = nmm.od().recvgithash();
      else
        o.githash.clear();
      
      
      o.vendor = nmm.od().vendor();
      o.owner = nmm.od().owner();
      o.remark = nmm.od().remark();


      idb.addValueObserver(nmm.sourceid(), "observer_details", {
          {"clock_offset_ns", o.clockOffsetNS},
            {"clock_drift_ns", o.clockOffsetDriftNS},
              {"clock_acc_ns", o.clockAccuracyNS},
                {"freq_acc_ps", o.freqAccuracyPS},
                  {"uptime", o.uptime}
        }, 
        nmm.localutcseconds());

      
    }
    else if(nmm.type() == NavMonMessage::UbloxJammingStatsType) {
      /*
      cout<<"noisePerMS "<<nmm.ujs().noiseperms() << " agcCnt "<<
        nmm.ujs().agccnt()<<" flags "<<nmm.ujs().flags()<<" jamind "<<
        nmm.ujs().jamind()<<endl;
      */
      idb.addValueObserver(nmm.sourceid(), "ubx_jamming", {
          {"noise_per_ms", nmm.ujs().noiseperms()},
            {"agccnt", nmm.ujs().agccnt()},
              {"jamind", nmm.ujs().jamind()},
                {"flags", nmm.ujs().flags()}
        }, 
        nmm.localutcseconds());

      
    }

    else if(nmm.type()== NavMonMessage::DebuggingType) {
      if(doGalileoReportSpeedup)
        continue; // speedup
      
      auto ret =  parseTrkMeas(basic_string<uint8_t>((const uint8_t*)nmm.dm().payload().c_str(), nmm.dm().payload().size()));
      for(const auto& tss : ret) {
        SatID id{static_cast<uint32_t>(tss.gnss), static_cast<uint32_t>(tss.sv), tss.gnss == 2 ? 1u : 0u};
        if(g_svstats[id].completeIOD()) {
          double freqMHZ =  1575.42;
          double tsat = ldexp(1.0* tss.tr, -32) /1000.0;
          auto res = g_svstats[id].doDoppler(tsat, g_srcfacts[nmm.sourceid()].pos, freqMHZ * 1000000);

          
          //          idb.addValueObserver((int)nmm.sourceid(), "orbit",
          //            {{"preddop", res.preddop},
          //               {"radvel", res.radvel}}, (time_t)nmm.localutcseconds(), id);

          //          cout << " preddop "<<res.preddop <<" meas "<<tss.dopplerHz <<" delta " << (tss.dopplerHz - res.preddop);
          double t = utcFromGPS(latestWN(0, g_svstats), tsat); 
          if(t - g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime > 5) { // only replace after 5 seconds
            g_svstats[id].perrecv[nmm.sourceid()].deltaHz =  tss.dopplerHz -  res.preddop;
            g_svstats[id].perrecv[nmm.sourceid()].deltaHzTime =  t;
            //            idb.addValue(id, "delta_hz", tss.dopplerHz -  res.preddop);

            auto corr = getHzCorrection(t, nmm.sourceid(), id.gnss, id.sigid, g_svstats);
            if(corr) {
              Point sat;
              g_svstats[id].getCoordinates(tsat, &sat);

              //              idb.addValue(id, "delta_hz_cor", tss.dopplerHz -  res.preddop - *corr, nmm.sourceid());

              if(doLogRFData)
              idb.addValue(id, "correlator",
                           {{"delta_hz_cor", tss.dopplerHz -  res.preddop - *corr},
                               {"delta_hz", tss.dopplerHz -  res.preddop},
                                 {"hz", tss.dopplerHz},
                                   {"elevation", getElevationDeg(sat, g_srcfacts[nmm.sourceid()].pos)},
                                     {"prres", g_svstats[id].perrecv[nmm.sourceid()].prres},
                                       {"qi", g_svstats[id].perrecv[nmm.sourceid()].qi},
                                         {"used", g_svstats[id].perrecv[nmm.sourceid()].used},
                                           {"db", g_svstats[id].perrecv[nmm.sourceid()].db}

                           }, t, nmm.sourceid());

              
            }
          }
        }
        //        cout<<endl;
      }
    }
    else if(nmm.type()== NavMonMessage::GPSInavType) {
      
      if(nmm.gpsi().sigid()) {
        cout<<"ignoring sigid "<<nmm.gpsi().sigid()<<" for legacy GPS "<<nmm.gpsi().gnsssv()<<endl;
        continue;
      }
      auto cond = getCondensedGPSMessage(std::basic_string<uint8_t>((uint8_t*)nmm.gpsi().contents().c_str(), nmm.gpsi().contents().size()));
      SatID id{nmm.gpsi().gnssid(), nmm.gpsi().gnsssv(), nmm.gpsi().sigid()};

      g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds();
      
      auto& svstat = g_svstats[id];
      svstat.gnss = id.gnss;
      auto oldsvstat = svstat;
      uint8_t page;
      auto& gm = svstat.gpsmsg;
      auto oldgm = gm;
      int frame = gm.parseGPSMessage(cond, &page);

      if(frame == 1) {
        
        idb.addValue(id, "clock", {{"offset_ns", getGPSAtomicOffset(svstat.tow(), svstat.gpsmsg).first}, 
              {"t0c", 16*gm.t0c},
                {"af0", 8*gm.af0},
                  {"af1", 8*gm.af1},
                    {"af2", 16*gm.af2}}, satUTCTime(id)); 

        //        cout<<"Got ura "<<gm.ura<<" for sv "<<id.first<<","<<id.second<<endl;
        idb.addValue(id, "gpsura", {{"value", gm.ura}}, satUTCTime(id));

        if(oldgm.af0 && oldgm.ura != gm.ura)  { // XX find better way to check
          cout<<humanTime(id.gnss, gm.wn, gm.tow)<<" src "<<nmm.sourceid()<<" wn "<<gm.wn <<" GPS "<<id.sv <<"@"<<id.sigid<<" change in URA ["<< humanUra(oldgm.ura) <<"] -> [" << humanUra(gm.ura)<<"] "<<(int)gm.ura<<", lastseen "<<ephAge(oldgm.tow, gm.tow)/3600.0 <<" hours"<<endl;
        }

        if(oldgm.af0 && oldgm.gpshealth != gm.gpshealth)  { // XX find better way to check
          cout<<humanTime(id.gnss, gm.wn, gm.tow)<<" src " <<nmm.sourceid()<<" wn "<<gm.wn <<" GPS "<<id.sv <<"@"<<id.sigid<<" change in health ["<< (int)oldgm.gpshealth <<"] -> [" << (int)gm.gpshealth <<"], lastseen "<<ephAge(oldgm.tow, gm.tow)/3600.0 <<" hours"<<endl;
        }


        //        double age = ephAge(g_svstats[id].tow, g_svstats[id].t0c * 16);

        if(oldgm.af0 && oldgm.t0c != gm.t0c) {
          auto oldOffset = getGPSAtomicOffset(gm.tow, oldgm);
          auto newOffset = getGPSAtomicOffset(gm.tow, gm);
          svstat.timeDisco = oldOffset.first - newOffset.first;
          if(fabs(svstat.timeDisco) < 10000) {
            idb.addValue(id, "clock_jump_ns", {
                    {"jump", svstat.timeDisco},
                    {"duration", ephAge(gm.t0c * 16, oldgm.t0c * 16)},
                    {"old-af0", oldgm.af0},
                    {"old-af1", oldgm.af1},
                    {"old-af2", oldgm.af2},
                    {"old-t0c", oldgm.t0c * 16},
                    {"new-af0", gm.af0},
                    {"new-af1", gm.af1},
                    {"new-af2", gm.af2},
                    {"new-t0c", gm.t0c * 16}

                  }, satUTCTime(id));

          }
        }
      }
      else if(frame==2) {
        if(oldgm.gpshealth != gm.gpshealth) {
          cout<<humanTime(id.gnss, gm.wn, gm.tow)<<" src "<<nmm.sourceid()<<" GPS "<<id.sv <<"@"<<id.sigid<<" change in health: ["<< (int)oldgm.gpshealth<<"] -> ["<< (int)gm.gpshealth<<"] , lastseen "<<ephAge(oldgm.tow, gm.tow)/3600.0 <<" hours"<<endl;
        }
        svstat.gpsmsg2 = gm;
        idb.addValue(id, "gpshealth", {{"value", g_svstats[id].gpsmsg.gpshealth}}, satUTCTime(id));

        if(svstat.gpsmsg2.gpsiod == svstat.gpsmsg3.gpsiod && gm.gpsiod != svstat.ephgpsmsg.gpsiod) {
          svstat.oldephgpsmsg = svstat.ephgpsmsg;
          svstat.ephgpsmsg = gm;
          svstat.reportNewEphemeris(id, idb);
        }

      }
      else if(frame == 3) {
        svstat.gpsmsg3 = gm;
        if(svstat.gpsmsg2.gpsiod == svstat.gpsmsg3.gpsiod && gm.gpsiod != svstat.ephgpsmsg.gpsiod) {
          svstat.oldephgpsmsg = svstat.ephgpsmsg;
          svstat.ephgpsmsg = gm;
          svstat.reportNewEphemeris(id, idb);
        }
      }
      else if(frame==4 && page==18) {
        const auto& sv = g_svstats[id];

        g_GPSUTCOffset = getGPSUTCOffset(sv.tow(), sv.wn(), gm).first;
        idb.addValue(id, "utcoffset", {
            {"a0", gm.a0},
              {"a1", gm.a1},
                {"delta", g_GPSUTCOffset}
          },
          satUTCTime(id));
      }
      else if((frame == 5 && page  <= 24) || (frame==4 && page >=25 && page<=32)) {
        g_gpsalma[gm.gpsalma.sv] = gm.gpsalma;
      }
      g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds();
      // XXX conversion possibly vital
      //      g_svstats[id].tow = nmm.gpsi().gnsstow();
      //      g_svstats[id].wn = nmm.gpsi().gnsswn();
      //      if(g_svstats[id].wn < 512) // XXX ROLLOVER
      //        g_svstats[id].wn += 2048;
    }
    else if(nmm.type() == NavMonMessage::RTCMMessageType) {
      RTCMMessage rm;
      rm.parse(nmm.rm().contents());
      if(rm.type == 1057 || rm.type == 1240) {
        for(const auto& ed : rm.d_ephs) {
          auto iter = g_svstats.find(ed.id);
          // XXX NAVCAST ONLY
          if(iter != g_svstats.end() && iter->second.completeIOD()  && iter->second.liveIOD().getIOD() == ed.iod && nmm.sourceid()==302)
            iter->second.rtcmEphDelta = ed;
          
          idb.addValue(ed.id, "rtcm-eph-correction", {
                       {"iod", ed.iod},
                       {"radial", ed.radial},
                       {"along", ed.along},
                       {"cross", ed.cross},
                       {"dradial", ed.dradial},
                       {"dalong", ed.dalong},
                       {"dcross", ed.dcross},
                         {"ssr-iod", rm.ssrIOD},
                       {"ssr-provider", rm.ssrProvider},
                       {"ssr-solution", rm.ssrSolution},
                       {"tow", rm.sow},
                         {"udi", rm.udi},
                           {"total-dist", sqrt(ed.radial*ed.radial + ed.along*ed.along + ed.cross*ed.cross)}},
            nmm.localutcseconds(),
            nmm.sourceid());

        }
      }
      else if(rm.type == 1058 || rm.type == 1241) {

        for(const auto& cd : rm.d_clocks) {
          auto iter = g_svstats.find(cd.id);
          if(iter != g_svstats.end() && nmm.sourceid()==302) /// XXX wrong
            iter->second.rtcmClockDelta = cd;

          idb.addValue(cd.id, "rtcm-clock-correction", {
                       {"dclock0", cd.dclock0},
                         {"dclock1", cd.dclock1},
                           {"dclock2", cd.dclock2},
                         {"ssr-iod", rm.ssrIOD},
                       {"ssr-provider", rm.ssrProvider},
                       {"ssr-solution", rm.ssrSolution},
                       {"tow", rm.sow},
                         {"udi", rm.udi}},
            nmm.localutcseconds(),
            nmm.sourceid());
          
        }
      }
      else if(rm.type == 1059 || rm.type == 1242) {
        for(const auto& dcb : rm.d_dcbs) {
          idb.addValue(dcb.first, "rtcm-dcb", {
              {"value", dcb.second}},
            nmm.localutcseconds(),
            nmm.sourceid());
        }
      }
      else if(rm.type == 1060 || rm.type == 1243) {
        for(const auto& ed : rm.d_ephs) {
          auto iter = g_svstats.find(ed.id);
          if(iter != g_svstats.end() && iter->second.completeIOD()  && iter->second.liveIOD().getIOD() == ed.iod && nmm.sourceid()==302)
            iter->second.rtcmEphDelta = ed;
          
          idb.addValue(ed.id, "rtcm-eph-correction", {
              {"iod", ed.iod},
                {"radial", ed.radial},
                  {"along", ed.along},
                    {"cross", ed.cross},
                      {"dradial", ed.dradial},
                        {"dalong", ed.dalong},
                          {"dcross", ed.dcross},
                            {"ssr-iod", rm.ssrIOD},
                              {"ssr-provider", rm.ssrProvider},
                                {"ssr-solution", rm.ssrSolution},
                                  {"tow", rm.sow},
                                    {"udi", rm.udi},
                                      {"total-dist", sqrt(ed.radial*ed.radial + ed.along*ed.along + ed.cross*ed.cross)}},
            nmm.localutcseconds(),
            nmm.sourceid());
          
        }
      }
      else if(rm.type == 1045 || rm.type == 1046) { // Galileo Ephemeris
        // rm.d_gm will now contain an at least partially filled out Galileo ephemeris
        // 1045 is F/NAV, 1046 is I/NAV
        // we have no real need for the I/NAV since we have that in spades

        if(rm.type == 1045) {
          const auto& eg = rm.d_gm;
          SatID id;
          id.gnss = 2;
          id.sv = rm.d_sv;
          id.sigid = 6; // seems reasonable for E5a

          static map<pair<int, int>, unsigned int> lastT0e;

          pair<int, int> key(nmm.sourceid(), rm.d_sv);
          if(!lastT0e.count(key) || lastT0e[key] != eg.t0e) {
            idb.addValue(id, "ephemeris-actual", {
              {"iod", eg.getIOD()}, 
                {"t0e", eg.t0e},
                  {"sqrta", eg.sqrtA},
                    {"e", eg.e},
                      {"cuc", eg.cuc},
                        {"cus", eg.cus},
                          {"crc", eg.crc},
                            {"crs", eg.crs},
                              {"m0", eg.m0},
                                {"deltan", eg.deltan},
                                  {"i0", eg.i0},
                                    {"cic", eg.cic},
                                      {"cis", eg.cis},
                                        {"omegadot", eg.omegadot},
                                          {"omega0", eg.omega0},
                                            {"idot", eg.idot},
                                              {"af0", eg.af0},
                                                {"af1", eg.af1},
                                                  {"af2", eg.af2},
                                                    {"t0c", eg.t0c},
                                                      {"omega", eg.omega}}, nmm.localutcseconds(), nmm.sourceid());
          }
          lastT0e[key] = eg.t0e;
        }
        
      }

      for(const auto& cd : rm.d_clocks) {
        auto iter = g_svstats.find(cd.id);
        if(iter != g_svstats.end() && nmm.sourceid()==302)
          iter->second.rtcmClockDelta = cd;
        
        idb.addValue(cd.id, "rtcm-clock-correction", {
            {"dclock0", cd.dclock0},
              {"dclock1", cd.dclock1},
                {"dclock2", cd.dclock2},
                  {"ssr-iod", rm.ssrIOD},
                    {"ssr-provider", rm.ssrProvider},
                      {"ssr-solution", rm.ssrSolution},
                        {"tow", rm.sow},
                          {"iod", cd.iod},
                            {"udi", rm.udi}},
          nmm.localutcseconds(),
          nmm.sourceid());
        
      }

    }
    else if(nmm.type()== NavMonMessage::GPSCnavType) {
      SatID id{nmm.gpsc().gnssid(), nmm.gpsc().gnsssv(), nmm.gpsc().sigid()};
      g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds();
      
      auto& svstat = g_svstats[id];
      svstat.gnss = id.gnss;

      GPSCNavState gcns;
      parseGPSCNavMessage(
                           std::basic_string<uint8_t>((uint8_t*)nmm.gpsc().contents().c_str(),
                                                      nmm.gpsc().contents().size()),
                           gcns);
      //      cout<<"Got a message from "<<makeSatIDName(id)<<endl;

      // XXX conversion possibly vital
      //      svstat.tow = nmm.gpsc().gnsstow();
      //      svstat.wn = nmm.gpsc().gnsswn();                         
      
    }
    else if(nmm.type()== NavMonMessage::BeidouInavTypeD1) {
      if(doGalileoReportSpeedup)
        continue; // speedup
            
    try {
      SatID id{nmm.bid1().gnssid(), nmm.bid1().gnsssv(), nmm.bid1().sigid()};

      g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds();
      
      auto& svstat = g_svstats[id];
      svstat.gnss = id.gnss;
      uint8_t pageno;
      auto cond = getCondensedBeidouMessage(std::basic_string<uint8_t>((uint8_t*)nmm.bid1().contents().c_str(), nmm.bid1().contents().size()));
      auto& bm = svstat.beidoumsg;
      auto oldbm = bm;
      int fraid=bm.parse(cond, &pageno);
      // XXX conversion possibly vital
      //      svstat.tow = nmm.bid1().gnsstow();
      //      svstat.wn = nmm.bid1().gnsswn();
      if(fraid == 1) {

        if(oldbm.sath1 != bm.sath1) {
          cout<<humanTime(id.gnss, svstat.wn(), svstat.tow())<<" wn "<<bm.wn<<" sow " <<bm.sow<<" BeiDou C"<<id.sv<<"@"<<id.sigid<<" health changed from  "<<(int)oldbm.sath1 <<" to "<< (int)bm.sath1 <<", lastseen "<<ephAge(oldbm.sow, bm.sow)/3600.0<<" hours"<<endl;
        }
        
        idb.addValue(id, "clock", {{"offset_ns", 1000000.0*bm.getAtomicOffset().first}, 
              {"t0c", bm.getT0c()},
                {"af0", bm.a0 * 2},
                  {"af1", bm.a1 / 16},
                    {"af2", bm.a2 / 128}}, satUTCTime(id)); // scaled to galileo units
        
        idb.addValue(id, "beidouhealth", {{"sath1", bm.sath1}}, satUTCTime(id));
        idb.addValue(id, "beidouurai", {{"value", bm.urai}}, satUTCTime(id));
        if(svstat.lastBeidouMessage1.wn >=0 && svstat.lastBeidouMessage1.t0c != bm.t0c) {
          auto oldOffset = svstat.lastBeidouMessage1.getAtomicOffset(bm.sow);
          auto newOffset = bm.getAtomicOffset(bm.sow);
          svstat.timeDisco = oldOffset.first - newOffset.first;
          if(fabs(svstat.timeDisco) < 10000)
            idb.addValue(id, "clock_jump_ns", {{"jump", svstat.timeDisco}}, satUTCTime(id));
        }
        svstat.lastBeidouMessage1 = bm;        
      }
      if(fraid == 2) {
        svstat.lastBeidouMessage2 = bm;
      }
      if(fraid == 3) {
        Point oldpoint, newpoint;
        if(bm.sow - svstat.lastBeidouMessage2.sow == 6) {
          if(svstat.ephBeidoumsg.getT0e() != svstat.beidoumsg.getT0e() && bm.sqrtA) {
            svstat.oldephBeidoumsg = svstat.ephBeidoumsg;
            svstat.ephBeidoumsg = bm;
            svstat.reportNewEphemeris(id, idb);
          }          
        }
      }
      else if((fraid == 4 && 1<= pageno && pageno <= 24) ||
              (fraid == 5 && 1<= pageno && pageno <= 6) ||
              (fraid == 5 && 11<= pageno && pageno <= 23) ) {

        struct BeidouAlmanacEntry bae;
        //        bm.alma.AmEpID = svstat.ephBeidoumsg.alma.AmEpID; // this comes from older messages
        
        if(processBeidouAlmanac(bm, bae)) {
          g_beidoualma[bae.sv]=bae;
        }
      }

      if(fraid==5 && pageno == 9) {
        /*
        svstat.a0g = bm.a0gps;
        svstat.a1g = bm.a1gps;        
        */
      }

      if(fraid==5 && pageno == 10) {
        /*
        svstat.a0 = bm.a0utc;
        svstat.a1 = bm.a1utc;
        */
        g_dtLSBeidou = bm.deltaTLS;
        g_BeiDouUTCOffset = bm.getUTCOffset(bm.sow).first;
        //        cout<<"Beidou leap seconds: "<<g_dtLSBeidou<<endl;
      }
      }catch(std::exception& e) {
      cerr<<"Beidou: "<<e.what()<<endl;
      }
    }
    else if(nmm.type()== NavMonMessage::BeidouInavTypeD2) {
      auto cond = getCondensedBeidouMessage(std::basic_string<uint8_t>((uint8_t*)nmm.bid2().contents().c_str(), nmm.bid2().contents().size()));
      /*
      int fraid = getbitu(&cond[0], beidouBitconv(16), 3);
      int sow = getbitu(&cond[0], beidouBitconv(19), 20);
      int pnum = getbitu(&cond[0], beidouBitconv(43), 4);
      int pre = getbitu(&cond[0], beidouBitconv(1), 11);

      //      cout<<"C"<< nmm.bid2().gnsssv() << " sent D2 message, pre "<<pre<<" sow "<<sow<<", FraID "<<fraid;
      //      if(fraid == 1)
      //        cout <<" pnum "<<pnum;
      //      cout<<endl;
      */
    }
    else if(nmm.type()== NavMonMessage::GlonassInavType) {
      if(doGalileoReportSpeedup)
        continue; // speedup

      SatID id{nmm.gloi().gnssid(), nmm.gloi().gnsssv(), nmm.gloi().sigid()};
      auto& svstat = g_svstats[id];
      svstat.gnss = id.gnss;
      auto& gm = svstat.glonassMessage;
      auto oldgm = gm;
      int strno = gm.parse(std::basic_string<uint8_t>((uint8_t*)nmm.gloi().contents().c_str(), nmm.gloi().contents().size()));
      g_svstats[id].perrecv[nmm.sourceid()].t = nmm.localutcseconds();
      if(strno == 1 && gm.n4 != 0 && gm.NT !=0) {
        //        uint32_t glotime = gm.getGloTime(); // this starts GLONASS time at 31st of december 1995, 00:00 UTC
        // CONVERSION, possibly vital
        //        svstat.wn = glotime / (7*86400);
        //        svstat.tow = glotime % (7*86400);
        //        cout<<"Glonass now: "<<humanTime(glotime + 820368000) << " n4 "<<(int)gm.n4<<" NT "<<(int)gm.NT<< " wn "<<svstat.wn <<" tow " <<svstat.tow<<endl;
      }
      else if(strno == 2) {
        if(svstat.wn() > 0)
          idb.addValue(id, "glohealth", {{"Bn", gm.Bn}}, satUTCTime(id));
        if(oldgm.Bn != gm.Bn) {
          cout<<humanTime(id.gnss, svstat.wn(), svstat.tow())<<" n4 "<< (int)gm.n4<<" NT " << (int)gm.NT<<" GLONASS R"<<id.sv<<"@"<<id.sigid<<" health changed from  "<<(int)oldgm.Bn <<" to "<< (int)gm.Bn <<endl;

        }
      }
      else if(strno == 4) {
        //        svstat.aode = gm.En * 24;
        if(svstat.wn() > 0) {
          idb.addValue(id, "glo_taun_ns", {{"value", gm.getTaunNS()}}, satUTCTime(id));
          idb.addValue(id, "ft", {{"value", gm.FT}}, satUTCTime(id));

          if(oldgm.taun && oldgm.taun != gm.taun) {
            if(gm.getGloTime() - oldgm.getGloTime()  < 300) {
              svstat.timeDisco = gm.getTaunNS() - oldgm.getTaunNS();
              idb.addValue(id, "clock_jump_ns", {{"jump", svstat.timeDisco}}, satUTCTime(id));
            }
          }
        }
        if(gm.x && gm.y && gm.z) {
          if(svstat.ephglomsg.x != gm.x &&
             svstat.ephglomsg.y != gm.y &&
             svstat.ephglomsg.z != gm.z) {
            svstat.oldephglomsg = svstat.ephglomsg;
            svstat.ephglomsg = gm;
            svstat.reportNewEphemeris(id, idb);
          }
        }
      }
      else if(strno == 5) {
        g_GlonassUTCOffset = gm.getUTCOffset(0).first;
        g_GlonassGPSOffset = gm.getGPSOffset(0).first;
        idb.addValue(id, "utcoffset", {
            {"tauc", gm.tauc},
                {"delta", g_GlonassUTCOffset}
          },
          satUTCTime(id));

      }
      else if(strno == 6 || strno == 8 || strno == 10 || strno == 12 || strno == 14) {
        svstat.glonassAlmaEven = {nmm.localutcseconds(), gm};
      }
      else if(strno == 7 || strno == 9 || strno == 11 || strno == 13 || strno == 15) {
        if(nmm.localutcseconds() - svstat.glonassAlmaEven.first < 4 && svstat.glonassAlmaEven.second.strtype == gm.strtype -1) {
          g_glonassalma[svstat.glonassAlmaEven.second.nA] = make_pair(svstat.glonassAlmaEven.second, gm);
        }
      }
      //      cout<<"GLONASS R"<<id.second<<" str "<<strno<<endl;
    }
    else if(nmm.type() == NavMonMessage::SBASMessageType) {
      if(doGalileoReportSpeedup)
        continue; // speedup

      auto& sb = g_sbas[nmm.sbm().gnsssv()];

      sb.perrecv[nmm.sourceid()].last_seen = nmm.localutcseconds();

      basic_string<uint8_t> sbas((uint8_t*)nmm.sbm().contents().c_str(), nmm.sbm().contents().length());
      auto delta = sb.status.parse(sbas, nmm.localutcseconds());
      // fast correction - clogs the database, so dropping that for now
      /*
      for(const auto& f : delta.first) {
        idb.addValue(f.id, "sbas_fast",
                     {{"correction", f.correction},
                         {"udrei", f.udrei}}, nmm.localutcseconds(),
                     nmm.sbm().gnsssv(), "sbas");
      }
      */
      for(const auto& lt : delta.second) {
        auto iter = g_svstats.find(lt.id);
        if(iter == g_svstats.end())
          continue;
        const auto& s = *iter;
        bool haveEphemeris=false;
        double spaceShift=0, ephShift = 0, rangeShift =0;
        if(s.second.completeIOD() && (s.second.liveIOD().getIOD() & 0xff) == lt.iod8) {
            Point sat;
            
            s.second.getCoordinates(s.second.tow(), &sat);
            Point adjsat=sat;
            adjsat.x += lt.dx;
            adjsat.y += lt.dy;
            adjsat.z += lt.dz;
            Point sbasCenter;
            int prn = nmm.sbm().gnsssv();
            if(prn== 126 || prn == 136 || prn == 123)
              sbasCenter = c_egnosCenter;
            else if(prn == 138 || prn == 131 || prn == 133)
              sbasCenter = c_waasCenter;
            else
              sbasCenter = Point{0,0,0};
            
            double dist = Vector(sbasCenter, adjsat).length() - Vector(sbasCenter, sat).length();
            spaceShift = dist;
            dist -= lt.dai / 3;
            ephShift = dist;
            rangeShift = dist - g_svstats[lt.id].sbas[nmm.sbm().gnsssv()].fast.correction;
            haveEphemeris=true;
          }
        idb.addValue(lt.id,"sbas_lterm",
                     {
                       {"iod8", lt.iod8},
                         {"toa", lt.toa},
                         {"iodp", lt.iodp},
                           {"dx", lt.dx},
                             {"dy", lt.dy},
                               {"dz", lt.dz},
                                 {"dai", lt.dai},
                                   {"ddx", lt.ddx},
                                     {"ddy", lt.ddy},
                                       {"ddz", lt.ddz},
                                         {"ddai", lt.ddai},
                                           {"ephemeris", 1.0*haveEphemeris},
                                             {"space_shift", spaceShift},
                                               {"eph_shift", ephShift},
                                                 {"range_shift", rangeShift}
                     }, nmm.localutcseconds(), nmm.sbm().gnsssv(), "sbas");

      }

      if(nmm.localutcseconds() - sb.status.d_lastDNU > 120) {
        for(const auto& c : sb.status.d_fast) {
          g_svstats[c.first].sbas[nmm.sbm().gnsssv()].fast = c.second; 
        }
        for(const auto& c : sb.status.d_longterm) {
          g_svstats[c.first].sbas[nmm.sbm().gnsssv()].longterm = c.second; 
        }
      }
    }
    else if(nmm.type() == NavMonMessage::SARResponseType) {
      SatID id;
      id.gnss = 2;
      id.sv = nmm.sr().gnsssv();
      id.sigid = nmm.sr().sigid();
      string hexid = nmm.sr().identifier();
      string hexstring;
      for(int n = 0; n < 15; ++n)                                   
        hexstring+=fmt::sprintf("%x", (int)getbitu((unsigned char*)hexid.c_str(), 4 + 4*n, 4));

      idb.addValue(id, "galsar", {{"mtype", (int) nmm.sr().type()},
            {"midentifier", hexstring},
              {"mcode", nmm.sr().code()},
                {"mparams", makeHexDump(nmm.sr().params())}}, satUTCTime(id), nmm.sourceid());

    }
    else if(nmm.type() == NavMonMessage::TimeOffsetType) {
      struct gnsstimeoffset
      {
        double offset{0};
        double accuracy{-1}; // this is how we detect 'unset'
      } gps, gal, glo, bds;
      for(const auto& o : nmm.to().offsets()) {
        if(o.gnssid() == 0) {
          gps.offset = o.offsetns();
          gps.accuracy = o.tacc();
        } else if(o.gnssid() == 2) {
          gal.offset = o.offsetns();
          gal.accuracy = o.tacc();
        } else if(o.gnssid() == 3) {
          bds.offset = o.offsetns();
          bds.accuracy = o.tacc();
        } else if(o.gnssid() == 6) {
          glo.offset = o.offsetns();
          glo.accuracy = o.tacc();
        }
      }
      idb.addValueObserver(nmm.sourceid(), "timeoffset",
      {{"itow", nmm.to().itow()},
          {gps.accuracy >= 0 ? "gps-offset" : nullptr, gps.offset},
          {gal.accuracy >= 0 ? "gal-offset" : nullptr, gal.offset},
          {glo.accuracy >= 0 ? "glo-offset": nullptr, glo.offset},
          {bds.accuracy >= 0 ? "bds-offset": nullptr, bds.offset},

          {"gps-tacc", gps.accuracy},
          {"gal-tacc", gal.accuracy},
          {"glo-tacc", glo.accuracy},
          {"bds-tacc", bds.accuracy},
            
          {(gal.accuracy >= 0 && gps.accuracy >=0) ? "gal-gps-offset" : nullptr, gal.offset - gps.offset},
          {(gal.accuracy >= 0 && bds.accuracy >=0) ? "gal-bds-offset" : nullptr, gal.offset - bds.offset},
          {(gal.accuracy >= 0 && glo.accuracy >=0) ? "gal-glo-offset" : nullptr, gal.offset - glo.offset},
          {(gps.accuracy >= 0 && bds.accuracy >=0) ? "gps-bds-offset" : nullptr, gps.offset - bds.offset},
          {(gps.accuracy >= 0 && glo.accuracy >=0) ? "gps-glo-offset" : nullptr, gps.offset - glo.offset},
          {(bds.accuracy >= 0 && glo.accuracy >=0) ? "bds-glo-offset" : nullptr, bds.offset - glo.offset}},
                           nmm.localutcseconds());
    }
    else {
      cout<<"Unknown type "<< (int)nmm.type()<<endl;
    }
    
  }
  }
  catch(EofException& e)
    {}
}
catch(std::exception& e)
{
  cerr<<"Exiting because of fatal error "<<e.what()<<endl;
}


// dedup techniques
#if 0
      /*
      struct DedupKey
      {
        SatID id;
        int wn;
        int tow;
        basic_string<uint8_t> contents;
        bool operator<(const DedupKey& rhs) const
        {
          return tie(id, wn, tow, contents) <
            tie(rhs.id, rhs.wn, rhs.tow, rhs.contents);
        }
      };

      static set<DedupKey> s_dedup;
      DedupKey dk{id, (int)nmm.gf().gnsswn(), (int)nmm.gf().gnsstow(), inav};
      if(s_dedup.insert(dk).second == false) {
        //        cout<<"Dedup"<<endl;
        continue;
      }
      if(s_dedup.size() > 10000)
        s_dedup.clear();
      */
      // XXX conversion, may be vital
      // g_svstats[id].wn = nmm.gf().gnsswn();
#endif
