text:
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/foreach.hpp>
#include <vector>

#include <common.hxx>
#include <fs-archive.hxx>
#include <output.hxx>
#include <retention.hxx>

using namespace pgbckctl;

/* ****************************************************************************
 * Implementation of OutputFormatter
 * ****************************************************************************/

OutputFormatter::OutputFormatter(std::shared_ptr<OutputFormatConfiguration> config,
                                 std::shared_ptr<BackupCatalog> catalog,
                                 std::shared_ptr<CatalogDescr> catalog_descr) {

  /*
   * NOTE: We allow a nullptr for catalog_descr, since it's not a hard requirement
   *       for every output format
   */

  /* We throw immediately in case catalog is uninitalized */
  if (catalog == nullptr)
    throw CCatalogIssue("undefined catalog handle not allowed in formatter instance");

  /* We throw immediately in case catalog wasn't opened */
  if (!catalog->available())
    throw CCatalogIssue("formatter instances expect catalog handler to be opened");

  /* We throw immediatly if there's no valid output config */
  if (config == nullptr)
    throw CCatalogIssue("formatter instances need a valid format config");

  this->catalog = catalog;
  this->catalog_descr = catalog_descr;
  this->config = config;

}

OutputFormatter::~OutputFormatter() {}

std::shared_ptr<OutputFormatter> OutputFormatter::formatter(std::shared_ptr<OutputFormatConfiguration> config,
                                                            std::shared_ptr<BackupCatalog> catalog,
                                                            OutputFormatType type) {
  return formatter(config,
                   catalog,
                   std::shared_ptr<CatalogDescr>(nullptr),
                   type);

}

std::shared_ptr<OutputFormatter> OutputFormatter::formatter(std::shared_ptr<OutputFormatConfiguration> config,
                                                            std::shared_ptr<BackupCatalog> catalog,
                                                            std::shared_ptr<CatalogDescr> catalog_descr,
                                                            OutputFormatType type) {
  /*
   * NOTE: We allow a nullptr for catalog_descr, since it's not a hard requirement
   *       for every output format
   */

  std::shared_ptr<OutputFormatter> formatter = nullptr;

  /* We throw immediately in case catalog is uninitalized */
  if (catalog == nullptr)
    throw CCatalogIssue("undefined catalog handle not allowed in formatter instance");

  /* We throw immediately in case catalog wasn't opened */
  if (!catalog->available())
    throw CCatalogIssue("formatter instances expect catalog handler to be opened");

  /* We throw immediatly if there's no valid output config */
  if (config == nullptr)
    throw CCatalogIssue("formatter instances need a valid format config");

  switch(type) {

  case OUTPUT_CONSOLE:

    formatter = std::make_shared<ConsoleOutputFormatter>(config, catalog, catalog_descr);
    break;

  case OUTPUT_JSON:

    formatter = std::make_shared<JsonOutputFormatter>(config, catalog, catalog_descr);
    break;

  }

  return formatter;

}

void OutputFormatter::nodeAs(std::exception &e,
                             std::ostringstream &output,
                             std::string output_type) {

  namespace pt = boost::property_tree;

  if (output_type == "json") {
    pt::ptree head;

    head.put("severity", "error");
    head.put("message", e.what());
    pt::write_json(output, head);
  }

  if (output_type == "console") {
    output << e.what() << endl;
  }

}

/* ****************************************************************************
 * Implementation of ConsoleOutputFormatter
 * ****************************************************************************/

ConsoleOutputFormatter::ConsoleOutputFormatter(std::shared_ptr<OutputFormatConfiguration> config,
                                               std::shared_ptr<BackupCatalog> catalog,
                                               std::shared_ptr<CatalogDescr> catalog_descr)
  : OutputFormatter(config, catalog, catalog_descr) {}

ConsoleOutputFormatter::~ConsoleOutputFormatter() {}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<std::list<directory_entry>> fileList,
                                    std::ostringstream &output) {

  for(auto item : *fileList) {

    if (is_regular_file(item.path())) {

      output << CPGBackupCtlBase::makeLine(boost::format("object=%60s type=f size=%d")
                                           % item.path() % file_size(item.path()));

    } else if (is_directory(item.path())) {

      output << CPGBackupCtlBase::makeLine(boost::format("object=%60s type=d")
                                           % item.path());

    }

  }

}

void ConsoleOutputFormatter::listBackupsVerbose(std::vector<std::shared_ptr<BaseBackupDescr>> &list,
                                                std::ostringstream &output) {

  /*
   * Format the output.
   */
  output << CPGBackupCtlBase::makeHeader("Basebackups in archive " + catalog_descr->archive_name,
                                       boost::format("%-20s\t%-60s") % "Property" % "Value",
                                       80);

  for (auto &basebackup : list) {

    size_t upstream_total_size = 0;

    /*
     * Directory handle for basebackup directory on disk.
     */
    StreamingBaseBackupDirectory directory(path(basebackup->fsentry).filename().string(),
                                           catalog_descr->directory);

    /*
     * Details for the backup profile used by the current basebackup.
     */
    shared_ptr<BackupProfileDescr> backupProfile
      = this->catalog->getBackupProfile(basebackup->used_profile);

    /*
     * Verify state of the current basebackup.
     */
    BaseBackupVerificationCode bbstatus
      = StreamingBaseBackupDirectory::verify(basebackup);

    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "ID" % basebackup->id);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Pinned" % ( (basebackup->pinned == 0) ? "NO" : "YES" ));
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Backup" % basebackup->fsentry);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Catalog State" % basebackup->status);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Label" % basebackup->label);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "WAL segment size" % basebackup->wal_segment_size);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Started" % basebackup->started);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Timeline" % basebackup->timeline);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "WAL start" % basebackup->xlogpos);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "WAL stop" % basebackup->xlogposend);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "System ID" % basebackup->systemid);
    output << CPGBackupCtlBase::makeLine(boost::format("%-20s\t%-60s")
                                       % "Used Backup Profile" % backupProfile->name);

    /*
     * Print tablespace information belonging to the current basebackup
     */
    output << CPGBackupCtlBase::makeHeader("tablespaces",
                                         boost::format("%-20s\t%-60s")
                                         % "tablespace property"
                                         % "value", 80);

    for (auto &tablespace : basebackup->tablespaces) {

      output << "---" << std::endl;

      /* Check for parent tablespace (also known as pg_default) */
      if (tablespace->spcoid == 0) {

        output << " - " << boost::format("%-20s\%-60s")
          % "upstream location" % "pg_default" << std::endl;
        output << " - " << boost::format("%-20s\%-60s")
          % "upstream size" % tablespace->spcsize << std::endl;

      } else {

        output << " - " << boost::format("%-20s\%-60s")
          % "oid" % tablespace->spcoid << std::endl;
        output << " - " << boost::format("%-20s\%-60s")
          % "upstream location" % tablespace->spclocation << std::endl;
        output << " - " << boost::format("%-20s\%-60s")
          % "upstream size" % tablespace->spcsize << std::endl;

      }

      output << "---" << std::endl;

      upstream_total_size += tablespace->spcsize;
    }

    output << "Summary:" << std::endl;
    output << boost::format("%-25s\t%-40s")
      % "Total size upstream:" % CPGBackupCtlBase::prettySize(upstream_total_size * 1024) << std::endl;

    /*
     * Display the state and local size of the basebackup. If the basebackup
     * doesn't exist anymore, print a warning instead.
     */
    if (bbstatus != BASEBACKUP_OK) {

      output << boost::format("%-25s\t%-40s")
        % CPGBackupCtlBase::stdout_red("Backup status (ON-DISK):", true)
        % CPGBackupCtlBase::stdout_red(BackupDirectory::verificationCodeAsString(bbstatus), true) << std::endl;

      output << boost::format("%-25s\t%-40s")
        % "Total local backup size:"
        % "NOT AVAILABLE" << std::endl;

    } else {

      output << boost::format("%-25s\t%-40s")
        % "Backup duration" % basebackup->duration << std::endl;

      output << boost::format("%-25s\t%-40s")
        % CPGBackupCtlBase::stdout_green("Backup status (ON-DISK):", true)
        % CPGBackupCtlBase::stdout_green(BackupDirectory::verificationCodeAsString(bbstatus), true)
             << std::endl;

      output << boost::format("%-25s\t%-40s")
        % "Total local backup size:"
        % CPGBackupCtlBase::prettySize(directory.size()) << std::endl;

    }

    output << CPGBackupCtlBase::makeLine(80) << std::endl;
    output << std::endl;

  }

}

void ConsoleOutputFormatter::nodeAs(std::vector<std::shared_ptr<ConnectionDescr>> connections,
                                    std::ostringstream &output) {

  /*
   * Print result header
   */
  output << "List of connections for archive \""
         << catalog_descr->archive_name
         << "\"" << endl;

  /*
   * XXX: createArchive() normally ensures that a
   *      catalog connection definition of type 'basebackup'
   *      (CONNECTION_TYPE_BASEBACKUP) exists, at least. But
   *      we don't rely on this fact, just loop through
   *      the results and spill them out...
   *
   *      getCatalogConnection() returns the shared pointers
   *      ordered by its type.
   */

  for (auto & con : connections) {

    /* item header */
    output << CPGBackupCtlBase::makeHeader("connection type " + con->type,
                                           boost::format("%-15s\t%-60s") % "Attribute" % "Setting",
                                           80) << endl;
    output << boost::format("%-15s\t%-60s") % "DSN" % con->dsn << endl;
    output << boost::format("%-15s\t%-60s") % "PGHOST" % con->pghost << endl;
    output << boost::format("%-15s\t%-60s") % "PGDATABASE" % con->pgdatabase << endl;
    output << boost::format("%-15s\t%-60s") % "PGUSER" % con->pguser << endl;
    output << boost::format("%-15s\t%-60s") % "PGPORT" % con->pgport << endl;

  }

}

void ConsoleOutputFormatter::listBackups(std::vector<std::shared_ptr<BaseBackupDescr>> &list,
                                         std::ostringstream &output) {

  output << CPGBackupCtlBase::makeHeader("Basebackups in archive " + catalog_descr->archive_name,
                                         boost::format("%-5s\t%-35s\t%-40s") % "ID" % "Backup" % "Size",
                                         80);

  for (auto &basebackup : list) {

    /*
     * Verify state of the current basebackup.
     */
    BaseBackupVerificationCode bbstatus
      = StreamingBaseBackupDirectory::verify(basebackup);
    std::string status = "";

    /*
     * Directory handle for basebackup directory on disk.
     */
    StreamingBaseBackupDirectory directory(path(basebackup->fsentry).filename().string(),
                                           catalog_descr->directory);

    /*
     * Transform basebackup status into its string representation.
     */
    if (bbstatus != BASEBACKUP_OK) {

      status = CPGBackupCtlBase::stdout_red(BackupDirectory::verificationCodeAsString(bbstatus), true);

      output << boost::format("%-5s\t%-35s\t%-40s")
        % basebackup->id
        % basebackup->fsentry
        % "N/A"
           << std::endl;

    } else {

      status = CPGBackupCtlBase::stdout_green(BackupDirectory::verificationCodeAsString(bbstatus), true);

      output << boost::format("%-5s\t%-35s\t%-40s")
        % basebackup->id
        % basebackup->fsentry
        % CPGBackupCtlBase::prettySize(directory.size())
           << std::endl;

    }

    /* Print compact list of basebackups */

    output << "- Details" << std::endl;

    /* Duration */
    output << boost::format("\t%-20s\t%-60s")
      % "Duration" % basebackup->duration
         << std::endl;

    /* Datetime basebackup started */
    output << boost::format("\t%-20s\t%-60s")
      % "Started"
      % basebackup->started
         << std::endl;

    output << boost::format("\t%-20s\t%-60s")
      % "Stopped"
      % basebackup->stopped
         << std::endl;

    output << boost::format("\t%-20s\t%-60s")
      %" Status"
      % status
         << std::endl;

    output << CPGBackupCtlBase::makeLine(80);
    output << std::endl;

  }

}

void ConsoleOutputFormatter::nodeAs(std::vector<shm_worker_area> &slots,
                                    std::ostringstream &output) {

  for(auto &worker : slots) {

    string archive_name = "N/A";

    if (worker.archive_id >= 0) {
      shared_ptr<CatalogDescr> archive_descr = this->catalog->existsById(worker.archive_id);

      if (archive_descr->id >= 0) {
        archive_name = archive_descr->archive_name;
      }
    }

    cout << "WORKER PID " << worker.pid
         << " | executing " << CatalogDescr::commandTagName(worker.cmdType)
         << " | archive name " << archive_name
         << " | archive ID " << worker.archive_id
         << " | started " << CPGBackupCtlBase::ptime_to_str(worker.started)
         << endl;

    /* Print child info, if any */
    for (unsigned int idx = 0; idx < MAX_WORKER_CHILDS; idx++) {

      sub_worker_info child_info = worker.child_info[idx];

      if (child_info.pid > 0) {
        cout << " `-> CHILD "
             << idx
             << " | PID "
             << child_info.pid
             << " | "
             << ((child_info.backup_id < 0) ? "no backup in use" : "backup used: ID="
                 + CPGBackupCtlBase::intToStr(child_info.backup_id))
             << endl;
      }
    }

  }

}

void ConsoleOutputFormatter::nodeAs(std::vector<std::shared_ptr<RetentionDescr>> &retentionList,
                                    std::ostringstream &output) {

    output << CPGBackupCtlBase::makeHeader("List of retention policies",
                                         boost::format("%-10s\t%-30s\t%-25s") % "ID" % "NAME" % "CREATED",
                                         80);

    for (auto retention : retentionList) {

      output << boost::format("%-10s\t%-30s\t%-25s")
        % retention->id % retention->name % retention->created << endl;

      if (retention->rules.size() > 0) {
        output << boost::format("%-80s") % "RULE(s):"
             << endl;
      }

      for (auto rule : retention->rules) {

        shared_ptr<Retention> instance = Retention::get(rule);

        output << CPGBackupCtlBase::makeLine(boost::format("%-10s\t%-70s")
                                           % " - " % instance->asString());
        output << CPGBackupCtlBase::makeLine(80) << endl;

      }

    }

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<RetentionDescr> retentionDescr,
                                    std::ostringstream &output) {

  /* Print contents of this policy */

  std::cout << CPGBackupCtlBase::makeHeader("Details of retention policy " + retentionDescr->name,
                                            boost::format("%-15s\t%-60s") % "Attribute" % "Setting",
                                            80) << std::endl;

  std::cout << boost::format("%-15s\t%-60s") % "ID" % retentionDescr->id << std::endl;
  std::cout << boost::format("%-15s\t%-60s") % "NAME" % retentionDescr->name << std::endl;
  std::cout << boost::format("%-15s\t%-60s") % "CREATED" % retentionDescr->created << std::endl;

  std::cout << std::endl;

  for (auto rule : retentionDescr->rules) {

    /*
     * Build a retention object out of this rule to get
     * its decomposed string representation.
     */
    shared_ptr<Retention> instance = Retention::get(rule);
    std::cout << boost::format("%-15s\t%-60s") % "RULE" % instance->asString() << std::endl;

  }

  std::cout << CPGBackupCtlBase::makeLine(80) << std::endl;

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<std::list<std::shared_ptr<CatalogDescr>>> list,
                                    std::ostringstream &output) {

  /*
   * We must distinguish between ARCHIVE_LIST, ARCHIVE_FILTERED_LIST and
   * ARCHIVE_DETAIL_LIST.
   */
  std::string mode;

  config->get("list_archive.mode")->getValue(mode);

  /* ARCHIVE_LIST and ARCHIVE_FILTERED_LIST are basically the same. */
  if (mode == "filtered" || mode == "full") {
    listArchiveList(list, output);
  }

  if (mode == "detail") {
    listArchiveDetail(list, output);
  }

}

void ConsoleOutputFormatter::listArchiveList(std::shared_ptr<std::list<std::shared_ptr<CatalogDescr>>> list,
                                             std::ostringstream &output) {

  /* Print headline */
  output << CPGBackupCtlBase::makeHeader("Filtered archive list",
                                         boost::format("%-15s\t%-30s") % "Name" % "Directory",
                                         80);

  /*
   * Print archive properties
   */
  for (auto& descr : *list) {
    output << CPGBackupCtlBase::makeLine(boost::format("%-15s\t%-30s")
                                         % descr->archive_name
                                         % descr->directory);
  }

}

void ConsoleOutputFormatter::listArchiveDetail(std::shared_ptr<std::list<std::shared_ptr<CatalogDescr>>> list,
                                               std::ostringstream &output) {

  /* Print headline */
  output << CPGBackupCtlBase::makeHeader("Detail view for archive",
                                       boost::format("%-20s\t%-30s") % "Property" % "Setting",
                                       80);

  for (auto& descr: *list) {

    output << CPGBackupCtlBase::makeLine(80) << endl;

    output << boost::format("%-20s\t%-30s") % "NAME" % descr->archive_name << endl;
    output << boost::format("%-20s\t%-30s") % "DIRECTORY" % descr->directory << endl;
    output << boost::format("%-20s\t%-30s") % "PGHOST" % descr->coninfo->pghost << endl;
    output << boost::format("%-20s\t%-30d") % "PGPORT" % descr->coninfo->pgport << endl;
    output << boost::format("%-20s\t%-30s") % "PGDATABASE" % descr->coninfo->pgdatabase << endl;
    output << boost::format("%-20s\t%-30s") % "PGUSER" % descr->coninfo->pguser << endl;
    output << boost::format("%-20s\t%-30s") % "DSN" % descr->coninfo->dsn << endl;
    output << boost::format("%-20s\t%-30s") % "COMPRESSION" % descr->compression << endl;
    output << CPGBackupCtlBase::makeLine(80) << endl;

    output << CPGBackupCtlBase::makeLine(80) << endl;

  }

}

void ConsoleOutputFormatter::nodeAs(std::vector<std::shared_ptr<BaseBackupDescr>> &list,
                                    std::ostringstream &output) {

  bool verbose;

  config->get("list_backups.verbose")->getValue(verbose);

  if (verbose) {

    this->listBackupsVerbose(list, output);

  } else {

    this->listBackups(list, output);

  }

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<RuntimeConfiguration> rtc,
                                    std::ostringstream &output) {

  auto it_start = rtc->begin();
  auto it_end = rtc->end();

  output << CPGBackupCtlBase::makeHeader("Runtime Variables",
                                       boost::format("%-30s\t%-40s") % "Name" % "Value",
                                       80);

  for(; it_start != it_end; ++it_start) {

    shared_ptr<ConfigVariable> var = it_start->second;
    string str_value;

    var->getValue(str_value);

    output << boost::format("%-30s | %-40s") % var->getName() % str_value
         << endl;

  }

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<ConfigVariable> var,
                                    std::ostringstream &output) {

  string str_value;

  /* extract value */
  var->getValue(str_value);

  output << CPGBackupCtlBase::makeHeader("Runtime Variables",
                                       boost::format("%-30s\t%-40s") % "Name" % "Value",
                                       80);
  output << boost::format("%-30s | %-40s") % var->getName() % str_value
         << endl;

}

void ConsoleOutputFormatter::nodeAs(std::string result_str,
                                    std::ostringstream &output) {

  output << result_str << endl;

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<BackupProfileDescr> profile,
                                    std::ostringstream &output) {

  output << CPGBackupCtlBase::makeHeader("Details for backup profile " + profile->name,
                                       boost::format("%-25s\t%-40s") % "Property" % "Setting", 80);

  /* Profile Name */
  output << boost::format("%-25s\t%-30s") % "NAME" % profile->name << endl;

  /* Profile compression type */
  output << boost::format("%-25s\t%-30s") % "COMPRESSION"
    % BackupProfileDescr::compressionType(profile->compress_type) << endl;

  /* Profile max rate */
  if (profile->max_rate <= 0) {
    output << boost::format("%-25s\t%-30s") % "MAX RATE" % "NOT RATED"<< endl;
  } else {
    output << boost::format("%-25s\t%-30s") % "MAX RATE(KByte/s)" % profile->max_rate<< endl;
  }

  /* Profile backup label */
  output << boost::format("%-25s\t%-30s") % "LABEL" % profile->label<< endl;

  /* Profile fast checkpoint mode */
  output << boost::format("%-25s\t%-30s") % "FAST CHECKPOINT" % profile->fast_checkpoint<< endl;

  /* Profile WAL included */
  output << boost::format("%-25s\t%-30s") % "WAL INCLUDED" % profile->include_wal<< endl;

  /* Profile WAIT FOR WAL */
  output << boost::format("%-25s\t%-30s") % "WAIT FOR WAL" % profile->wait_for_wal<< endl;

  /* Profile NOVERIFY */
  output << boost::format("%-25s\t%-30s") % "NOVERIFY CHECKSUMS" % profile->noverify_checksums<< endl;

  /* Profile MANIFEST */
  output << boost::format("%-25s\t%-30s") % "MANIFEST" % profile->manifest<< endl;

  /* Profile MANIFEST_CHECKSUMS */
  output << boost::format("%-25s\t%-30s") % "MANIFEST CHECKSUMS" % profile->manifest_checksums<< endl;

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<std::list<std::shared_ptr<BackupProfileDescr>>> &list,
                                    std::ostringstream &output) {

  /*
   * Print headline
   */
  output << CPGBackupCtlBase::makeHeader("List of backup profiles",
                                       boost::format("%-25s\t%-15s")
                                       % "Name" % "Backup Label", 80);

  /*
   * Print name and label
   */
  for (auto& descr : *list) {
    output << boost::format("%-25s\t%-15s") % descr->name % descr->label
         << endl;
  }

}

void ConsoleOutputFormatter::nodeAs(std::shared_ptr<StatCatalogArchive> stat,
                                    std::ostringstream &output) {

  output << CPGBackupCtlBase::makeHeader("Archive Catalog Overview",
                                            boost::format("%-15s\t%-30s\t%-20s")
                                            % "Name" % "Directory" % "Host", 80);
  output << boost::format("%-15s\t%-30s\t%-20s")
    % stat->archive_name % stat->archive_directory % stat->archive_host;
  output << endl;

  /*
   * Catalog statistics data.
   */
  output << CPGBackupCtlBase::makeHeader("Backups",
                                            boost::format("%-9s\t%-9s\t%-9s\t%-16s")
                                            % "# total" % "# failed"
                                            % "# running" % "avg duration (s)", 80);
  output << boost::format("%-9s\t%-9s\t%-9s\t%-16s")
    % stat->number_of_backups % stat->backups_failed
    % stat->backups_running
    % stat->avg_backup_duration;
  output << endl;

}

/* ****************************************************************************
 * Implementation of JsonOutputFormatter
 * ****************************************************************************/

JsonOutputFormatter::JsonOutputFormatter(std::shared_ptr<OutputFormatConfiguration> config,
                                         std::shared_ptr<BackupCatalog> catalog,
                                         std::shared_ptr<CatalogDescr> catalog_descr)
  : OutputFormatter(config, catalog, catalog_descr) {}

JsonOutputFormatter::~JsonOutputFormatter() {}

void JsonOutputFormatter::nodeAs(std::shared_ptr<std::list<directory_entry>> fileList,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  std::ostringstream num_objects;
  std::map<path, std::list<pt::ptree>> directory_lookup;

  num_objects << fileList->size();
  head.put("num filesystem objects", num_objects.str());

  for(auto item : *fileList) {

    path item_path = item.path();
    pt::ptree fentry;

    if (is_regular_file(item_path)) {

      fentry.put("name", item_path.filename().string());
      fentry.put("size", file_size(item_path));
      fentry.put("type", "file");

      auto lookup = directory_lookup.find(item_path.parent_path());

      /**
       * Lookup directory if already visited.
       */
      if (lookup != directory_lookup.end()) {

        lookup->second.push_back(fentry);

      } else {

        std::list<pt::ptree> fentry_list;
        fentry_list.push_back(fentry);
        directory_lookup[item_path.parent_path()] = fentry_list;

      }

    } else if (is_directory(item_path)) {

        std::list<pt::ptree> fentry_list; /* empty list */
        directory_lookup[item_path] = fentry_list;

    }

  }

  for (auto path_item : directory_lookup) {

    pt::ptree objects;

    for (auto fileobj : path_item.second) {

      if (path_item.second.size() > 0)
        objects.add_child(pt::ptree::path_type(fileobj.get<std::string>("name"), '|'), fileobj);

    }

    num_objects.str("");
    num_objects.clear();
    num_objects << path_item.second.size();

    objects.add("num files", num_objects.str());
    head.add_child(path_item.first.string(), objects);

  }

  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::vector<std::shared_ptr<ConnectionDescr>> connections,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree clist; /* makes up json array for connection info */
  std::ostringstream oss;

  oss << connections.size();

  head.put("num_connections", oss.str());
  head.put("archive name", catalog_descr->archive_name);

  for (auto & con : connections) {

    pt::ptree item;

    item.put("connection type", con->type);
    item.put("hostname", con->pghost);
    item.put("dbname", con->pgdatabase);
    item.put("user", con->pguser);
    item.put("port", con->pgport);
    item.put("dsn", con->dsn);

    clist.push_back(std::make_pair("", item));

  }

  head.add_child("connections", clist);
  pt::write_json(output, head);

}

void JsonOutputFormatter::listBackups(std::vector<std::shared_ptr<BaseBackupDescr>> &list,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree basebackups;
  std::ostringstream oss;

  oss << list.size();

  head.put("num_basebackups", oss.str());

  BOOST_FOREACH(const std::shared_ptr<BaseBackupDescr> &descr, list) {

    StreamingBaseBackupDirectory directory(path(descr->fsentry).filename().string(),
                                           catalog_descr->directory);
    pt::ptree bbackup;

    oss.str(std::string());
    oss << descr->id;

    bbackup.put("id", oss.str());
    bbackup.put("fsentry", descr->fsentry);
    bbackup.put("started", descr->started);
    bbackup.put("stopped", descr->stopped);
    bbackup.put("duration", descr->duration);

    oss.str("");
    oss.clear();
    oss << descr->tablespaces.size();

    bbackup.put("num_tablespaces", oss.str());
    bbackup.put("size", directory.size());
    bbackup.put("status",
                BackupDirectory::verificationCodeAsString(StreamingBaseBackupDirectory::verify(descr)));

    basebackups.push_back(std::make_pair("", bbackup));
  }

  head.add_child("basebackups", basebackups);

  pt::write_json(output, head);

}

void JsonOutputFormatter::listBackupsVerbose(std::vector<std::shared_ptr<BaseBackupDescr>> &list,
                                             std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree basebackups;
  std::ostringstream oss;

  oss << list.size();
  head.put("num_basebackups", oss.str());

  BOOST_FOREACH(const std::shared_ptr<BaseBackupDescr> &descr, list) {

    size_t upstream_total_size = 0;
    pt::ptree bbackup;
    pt::ptree tablespaces;

    /*
     * Directory handle for basebackup directory on disk.
     */
    StreamingBaseBackupDirectory directory(path(descr->fsentry).filename().string(),
                                           catalog_descr->directory);

    /*
     * Details for the backup profile used by the current basebackup.
     */
    shared_ptr<BackupProfileDescr> backupProfile
      = this->catalog->getBackupProfile(descr->used_profile);

    bbackup.put("id", oss.str());
    bbackup.put("pinned", (descr->pinned ? "yes" : "no"));
    bbackup.put("fsentry", descr->fsentry);
    bbackup.put("catalog state", descr->status);
    bbackup.put("label", descr->label);
    bbackup.put("started", descr->started);
    bbackup.put("stopped", descr->stopped);
    bbackup.put("duration", descr->duration);
    bbackup.put("timeline", descr->timeline);
    bbackup.put("wal start location", descr->xlogpos);
    bbackup.put("wal stop location", descr->xlogposend);
    bbackup.put("system id", descr->systemid);
    bbackup.put("wal segment size", descr->wal_segment_size);
    bbackup.put("size", directory.size());
    bbackup.put("status",
                BackupDirectory::verificationCodeAsString(StreamingBaseBackupDirectory::verify(descr)));

    oss.str("");
    oss.clear();
    oss << descr->tablespaces.size();

    bbackup.put("num_tablespaces", oss.str());

    BOOST_FOREACH(const std::shared_ptr<BackupTablespaceDescr> &tblspc, descr->tablespaces) {

      pt::ptree tblspcinfo;

      tblspcinfo.put("tablespace oid", tblspc->spcoid);
      tblspcinfo.put("tablespace size", tblspc->spcsize);

      /* Check for parent tablespace (also known as pg_default) */
      if (tblspc->spcoid == 0) {
        tblspcinfo.put("tablespace location", "pg_default");
      } else {
        tblspcinfo.put("tablespace location", tblspc->spclocation);
      }

      upstream_total_size += tblspc->spcsize;
      tablespaces.push_back(std::make_pair("", tblspcinfo));

    }

    oss.str("");
    oss.clear();
    oss << upstream_total_size;
    bbackup.put("upstream total size", oss.str());
    bbackup.push_back(std::make_pair("", tablespaces));
    basebackups.push_back(std::make_pair("", bbackup));

  }

  head.add_child("basebackups", basebackups);
  pt::write_json(output, head);

}

boost::property_tree::ptree JsonOutputFormatter::toPtree(std::shared_ptr<RetentionDescr> retentionDescr) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree rules;

  head.put("id", retentionDescr->id);
  head.put("name", retentionDescr->name);
  head.put("created", retentionDescr->created);

  BOOST_FOREACH(const std::shared_ptr<RetentionRuleDescr> &rule, retentionDescr->rules) {

    /*
     * Build a retention object out of this rule to get
     * its decomposed string representation.
     */
    shared_ptr<Retention> instance = Retention::get(rule);
    rules.put("rule", instance->asString());

  }

  head.add_child("rules", rules);

  return head;

}

void JsonOutputFormatter::nodeAs(std::vector<std::shared_ptr<RetentionDescr>> &retentionList,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree item;

  head.put("number of retentions", retentionList.size());

  BOOST_FOREACH(const std::shared_ptr<RetentionDescr> descr, retentionList) {
    item.add_child("retention", this->toPtree(descr));
  }

  head.add_child("retentions", item);
  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<RetentionDescr> retentionDescr,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree result = this->toPtree(retentionDescr);
  pt::write_json(output, result);

}

void JsonOutputFormatter::nodeAs(std::vector<shm_worker_area> &slots,
                                    std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree workers;

  head.put("number of background workers", slots.size());

  if (slots.size() > 0) {
    for(auto &worker : slots) {

      string archive_name      = "N/A";
      unsigned int child_count = 0;
      pt::ptree current;
      pt::ptree child_node;

      if (worker.archive_id >= 0) {
        shared_ptr<CatalogDescr> archive_descr = this->catalog->existsById(worker.archive_id);

        if (archive_descr->id >= 0) {
          archive_name = archive_descr->archive_name;
        }
      }

      current.put("worker id", worker.pid);
      current.put("command tag", CatalogDescr::commandTagName(worker.cmdType));
      current.put("archive name", archive_name);
      current.put("archive id", worker.archive_id);
      current.put("started", CPGBackupCtlBase::ptime_to_str(worker.started));

      /* Print child info, if any */
      for (unsigned int idx = 0; idx < MAX_WORKER_CHILDS; idx++) {

        sub_worker_info child_info = worker.child_info[idx];

        if (child_info.pid > 0) {
          child_count++;

          child_node.put("child id", idx);
          child_node.put("child pid", child_info.pid);
          child_node.put("attached basebackup id", child_info.backup_id);

        }
      }

      if (child_count > 0) {
        current.put("number of childs", child_count);
        current.push_back(std::make_pair("", child_node));
      }

      workers.push_back(std::make_pair("", current));
    }

    head.add_child("background workers",  workers);
  }

  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<std::list<std::shared_ptr<CatalogDescr>>> list,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree archives;

  /*
   * We must distinguish between ARCHIVE_LIST, ARCHIVE_FILTERED_LIST and
   * ARCHIVE_DETAIL_LIST.
   */
  std::string mode;

  config->get("list_archive.mode")->getValue(mode);

  for (auto &descr : *list) {

    pt::ptree item;

    head.put("number of archives", list->size());

    /* ARCHIVE_LIST and ARCHIVE_FILTERED_LIST are basically the same. */
    if (mode == "filtered" || mode == "full") {

      item.put("name", descr->archive_name);
      item.put("directory", descr->directory);

    } else {

      item.put("name", descr->archive_name);
      item.put("directory", descr->directory);
      item.put("pghost", descr->coninfo->pghost);
      item.put("pgport", descr->coninfo->pgport);
      item.put("pgdatabase", descr->coninfo->pgdatabase);
      item.put("pguser", descr->coninfo->pguser);
      item.put("dsn", descr->coninfo->dsn);
      item.put("wal compression", descr->compression);

    }

    archives.add_child(descr->archive_name, item);

  }

  head.add_child("archives", archives);
  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::vector<std::shared_ptr<BaseBackupDescr>> &list,
                                 std::ostringstream &output) {

  bool verbose;

  /*
   * Sanity check, make sure we have a valid catalog descriptor.
   */
  if (catalog_descr == nullptr)
    throw CCatalogIssue("could not format basebackup list without valid catalog descriptor");

  config->get("list_backups.verbose")->getValue(verbose);

  if (verbose) {

    this->listBackupsVerbose(list, output);

  } else {

    this->listBackups(list, output);

  }

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<RuntimeConfiguration> rtc,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  auto it_start = rtc->begin();
  auto it_end = rtc->end();

  pt::ptree head;
  head.put("number of variables", rtc->count_variables());

  for(; it_start != it_end; ++it_start) {

    pt::ptree item;

    shared_ptr<ConfigVariable> var = it_start->second;
    string str_value;

    var->getValue(str_value);

    item.put("value", str_value);

    head.add_child(var->getName(), item);

  }

  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<ConfigVariable> var,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  pt::ptree item;
  string str_value;

  var->getValue(str_value);
  item.put("name", var->getName());
  item.put("value", str_value);

  head.add_child("config variable", item);
  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::string result_str,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;
  head.put("result", result_str);

  pt::write_json(output, head);

}

void JsonOutputFormatter::listBackupProfileDetail(std::shared_ptr<BackupProfileDescr> descr,
                                                  boost::property_tree::ptree &node) {

  node.put("compress type", BackupProfileDescr::compressionType(descr->compress_type));
  node.put("max rate", descr->max_rate);
  node.put("backup label", descr->label);
  node.put("fast checkpoint", descr->fast_checkpoint);
  node.put("wal included", descr->include_wal);
  node.put("wait for wal", descr->wait_for_wal);
  node.put("noverify checksums", descr->noverify_checksums);
  node.put("manifest", descr->manifest);
  node.put("manifest checksums", descr->manifest_checksums);

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<BackupProfileDescr> profile,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;

  pt::ptree head;

  head.put("profile name", profile->name);
  this->listBackupProfileDetail(profile, head);

  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<std::list<std::shared_ptr<BackupProfileDescr>>> &list,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;
  pt::ptree head;

  head.put("number of profiles", list->size());

  for(auto &descr : *list) {

    pt::ptree item;

    this->listBackupProfileDetail(descr, item);
    head.add_child(descr->name, item);

  }

  pt::write_json(output, head);

}

void JsonOutputFormatter::nodeAs(std::shared_ptr<StatCatalogArchive> stat,
                                 std::ostringstream &output) {

  namespace pt = boost::property_tree;
  pt::ptree head;
  pt::ptree stats;

  head.put("archive name", stat->archive_name);
  head.put("directory", stat->archive_directory);
  head.put("host", stat->archive_host);

  stats.put("number of backups", stat->number_of_backups);
  stats.put("backups running", stat->backups_running);
  stats.put("avg duration", stat->avg_backup_duration);
  stats.put("backups failed", stat->backups_failed);

  head.add_child("backup statistics", stats);

  pt::write_json(output, head);

}

ext: cxx


text:
#include <iostream>
#include <iomanip>
#include <string>
#include <fstream>
#include <filesystem>
#include <vector>
#include <array>
#include <cmath>
#include <numeric>
#include <algorithm>
#include <cassert>
#include <chrono>
#include <thread>
#include <boost/format.hpp>
#include <GraphMol/SmilesParse/SmilesParse.h>
#include <GraphMol/FileParsers/MolSupplier.h>
#include <GraphMol/Fingerprints/MorganFingerprints.h>
#include <GraphMol/Substruct/SubstructMatch.h>
#include <Numerics/Alignment/AlignPoints.h>
#include <GraphMol/MolTransforms/MolTransforms.h>
#include <GraphMol/FileParsers/MolWriters.h>
#include <GraphMol/Depictor/RDDepictor.h>
#include <GraphMol/FragCatalog/FragFPGenerator.h>
#include <GraphMol/Descriptors/MolDescriptors.h>
#include <mongocxx/instance.hpp>
#include <mongocxx/pool.hpp>
#include <mongocxx/client.hpp>
#include <bsoncxx/json.hpp>
#include "utility.hpp"
#include "safe_counter.hpp"
#include "io_service_pool.hpp"
#include "compound_database.hpp"
using namespace std;
using namespace std::chrono;
using namespace std::filesystem;
using namespace RDKit;
using namespace RDKit::MolOps;
using namespace RDKit::MorganFingerprints;
using namespace RDGeom;
using namespace RDNumeric::Alignments;
using namespace MolTransforms;
using namespace RDKit::Descriptors;
using namespace mongocxx;
using bsoncxx::builder::basic::kvp;

template<typename T>
auto dist2(const T& p0, const T& p1)
{
	const auto d0 = p0[0] - p1[0];
	const auto d1 = p0[1] - p1[1];
	const auto d2 = p0[2] - p1[2];
	return d0 * d0 + d1 * d1 + d2 * d2;
}

array<Point3D, 4> calcRefPoints(const ROMol& mol, const vector<int>& heavyAtoms)
{
	const auto num_points = heavyAtoms.size();
	assert(num_points == mol.getNumHeavyAtoms());
	const auto& conf = mol.getConformer();
	array<Point3D, 4> refPoints;
	for (auto& ref : refPoints)
	{
		assert(ref[0] == 0);
		assert(ref[1] == 0);
		assert(ref[2] == 0);
	}
	auto& ctd = refPoints[0];
	auto& cst = refPoints[1];
	auto& fct = refPoints[2];
	auto& ftf = refPoints[3];
	for (const auto i : heavyAtoms)
	{
		const auto& a = conf.getAtomPos(i);
		ctd += a;
	}
	ctd /= static_cast<double>(num_points);
	auto cst_dist = numeric_limits<double>::max();
	auto fct_dist = numeric_limits<double>::lowest();
	auto ftf_dist = numeric_limits<double>::lowest();
	for (const auto i : heavyAtoms)
	{
		const auto& a = conf.getAtomPos(i);
		const auto this_dist = dist2(a, ctd);
		if (this_dist < cst_dist)
		{
			cst = a;
			cst_dist = this_dist;
		}
		if (this_dist > fct_dist)
		{
			fct = a;
			fct_dist = this_dist;
		}
	}
	for (const auto i : heavyAtoms)
	{
		const auto& a = conf.getAtomPos(i);
		const auto this_dist = dist2(a, fct);
		if (this_dist > ftf_dist)
		{
			ftf = a;
			ftf_dist = this_dist;
		}
	}
	return refPoints;
}

int main(int argc, char* argv[])
{
	// Check the required number of command line arguments.
	if (argc != 6)
	{
		cout << "lbvs databases host port user pass" << endl;
		return 0;
	}

	// Fetch command line arguments.
	const path cpdbs_path = argv[1];
	const string host = argv[2];
	const string port = argv[3];
	const string user = argv[4];
	const string pass = argv[5];

	// Initialize constants.
	cout << local_time_string() << "Initializing" << endl;
	const size_t num_usrs = 2;
	const array<string, 2> usr_names{{ "USR", "USRCAT" }};
	constexpr array<size_t, num_usrs> qn{{ 12, 60 }};
	constexpr array<double, num_usrs> qv{{ 1.0 / qn[0], 1.0 / qn[1] }};
	const size_t num_refPoints = 4;
	const size_t num_subsets = 5;
	const array<string, num_subsets> SubsetSMARTS
	{{
		"[!#1]", // heavy
		"[#6+0!$(*~[#7,#8,F]),SH0+0v2,s+0,S^3,Cl+0,Br+0,I+0]", // hydrophobic
		"[a]", // aromatic
		"[$([O,S;H1;v2]-[!$(*=[O,N,P,S])]),$([O,S;H0;v2]),$([O,S;-]),$([N&v3;H1,H2]-[!$(*=[O,N,P,S])]),$([N;v3;H0]),$([n,o,s;+0]),F]", // acceptor
		"[N!H0v3,N!H0+v4,OH+0,SH+0,nH+0]", // donor
	}};
	const size_t num_hits = 100;

	// Wrap SMARTS strings to RWMol objects.
	array<unique_ptr<ROMol>, num_subsets> SubsetMols;
	for (size_t k = 0; k < num_subsets; ++k)
	{
		SubsetMols[k].reset(reinterpret_cast<ROMol*>(SmartsToMol(SubsetSMARTS[k])));
	}

	// Read compound database directory.
	vector<compound_database> databases;
	for (directory_iterator cpdbs_dir_iter(cpdbs_path), end_dir_iter; cpdbs_dir_iter != end_dir_iter; ++cpdbs_dir_iter)
	{
		// Filter out non directory.
		if (!cpdbs_dir_iter->is_directory()) continue;

		// Create a compound database instance.
		databases.emplace_back(cpdbs_dir_iter->path());
	}
	string db_op_in_array;
	for (size_t i = 0; i < databases.size(); ++i)
	{
		db_op_in_array += (i ? ",\"" : "\"") + databases[i].name + "\"";
	}

	// Connect to mongodb and authenticate user.
	cout << local_time_string() << "Connecting to " << host << ':' << port << " and authenticating " << user << endl;
	const instance inst; // The constructor and destructor initialize and shut down the driver. http://mongocxx.org/api/current/classmongocxx_1_1instance.html
	const uri uri("mongodb://" + host + ":" + port + "/?minPoolSize=0&maxPoolSize=2"); // When connecting to a replica set, it is much more efficient to use a pool as opposed to manually constructing client objects. TODO: Create a credential that will authenticate properly regardless of server version, use a connection string with the user and password directly in the URI and with a parameter specifying the database to authenticate from: "mongodb://user:pass@localhost:27017/?authSource=jstar"
	pool pool(uri);
	const auto client = pool.acquire(); // Return value of acquire() is an instance of entry. An entry is a handle on a client object acquired via the pool.
	auto coll = client->database("jstar").collection("lbvs");
	const auto jobid_filter = bsoncxx::from_json(R"({ "startDate" : { "$exists" : false }, "database": { "$in": [)" + db_op_in_array + R"(] }})");
	const auto jobid_foau_options = options::find_one_and_update().sort(bsoncxx::from_json(R"({ "submitDate" : 1 })")).projection(bsoncxx::from_json(R"({ "_id" : 1, "qryMolSdf": 1, "database": 1, "score": 1 })")); // By default, the original document is returned

	// Initialize variables.
	array<vector<int>, num_subsets> subsets;
	array<vector<double>, num_refPoints> dista;
	alignas(32) array<double, 60> q; // The usrcat feature vector of the query molecule.

	// Initialize an io service pool and create worker threads for later use.
	const size_t num_threads = thread::hardware_concurrency();
	cout << local_time_string() << "Creating an io service pool of " << num_threads << " worker threads" << endl;
	io_service_pool io(num_threads);
	safe_counter<size_t> cnt;

	// Enter event loop.
	cout << local_time_string() << "Entering event loop" << endl;
	cout.setf(ios::fixed, ios::floatfield);
	bool sleeping = false;
	while (true)
	{
		// Fetch an incompleted job in a first-come-first-served manner.
		if (!sleeping) cout << local_time_string() << "Fetching an incompleted job" << endl;
		const auto startDate = system_clock::now();
		bsoncxx::builder::basic::document jobid_update_builder;
		jobid_update_builder.append(
			kvp("$set", [=](bsoncxx::builder::basic::sub_document set_subdoc) {
				set_subdoc.append(kvp("daemon", "cpp"));
				set_subdoc.append(kvp("startDate", bsoncxx::types::b_date(startDate)));
			})
		);

		// http://mongocxx.org/api/current/classmongocxx_1_1collection.html#a6b04b5265e044c08d1ecd1e4185fb699
		const auto jobid_document = coll.find_one_and_update(jobid_filter.view(), jobid_update_builder.extract(), jobid_foau_options);
		if (!jobid_document)
		{
			// No incompleted jobs. Sleep for a while.
			if (!sleeping) cout << local_time_string() << "Sleeping" << endl;
			sleeping = true;
			this_thread::sleep_for(std::chrono::seconds(2));
			continue;
		}
		sleeping = false;
		const auto jobid_view = jobid_document->view();

		// Obtain job properties.
		const auto _id = jobid_view["_id"].get_oid().value;
		cout << local_time_string() << "Executing job " << _id.to_string() << endl;
		const auto qry_mol_sdf = jobid_view["qryMolSdf"].get_utf8().value; // get_utf8().value returns an instance of std::string_view.
		const auto cpdb_name = jobid_view["database"].get_utf8().value;
		const auto score = jobid_view["score"].get_utf8().value;
		assert(score.compare("USR") == 0 || score.compare("USRCAT") == 0);
		const size_t usr0 = score.compare("USRCAT") == 0; // Specify the primary sorting score. 0: USR; 1: USRCAT.
		const auto usr1 = usr0 ^ 1;
		const auto qnu0 = qn[usr0];
		const auto qnu1 = qn[usr1];

		// Obtain a constant reference to the selected database.
		cout << local_time_string() << "Finding the selected compound database" << endl;
		size_t cpdb_index = 0;
		while (cpdb_name.compare(databases[cpdb_index].name) != 0) ++cpdb_index;
		const auto& cpdb = databases[cpdb_index];

		// Read the user-supplied SDF file.
		cout << local_time_string() << "Reading the query file" << endl;
		istringstream qry_mol_sdf_iss(qry_mol_sdf.data()); // data() may return a pointer to a buffer that is not null-terminated. Therefore it is typically a mistake to pass data() to a routine that takes just a const CharT* and expects a null-terminated string.
		SDMolSupplier qry_mol_sup(&qry_mol_sdf_iss, false, true, false, true); // takeOwnership, sanitize, removeHs, strictParsing. Note: setting removeHs=true (which is the default setting) will lead to fewer hydrogen bond acceptors being matched.

		// Initialize vectors to store compounds' primary score and their corresponding conformer.
		vector<double> scores(cpdb.num_compounds); // Primary score of compounds.
		vector<size_t> cnfids(cpdb.num_compounds); // ID of conformer with the best primary score.
		const auto compare = [&](const size_t val0, const size_t val1) // Sort by the primary score.
		{
			return scores[val0] < scores[val1];
		};

		// Initialize the number of chunks and the number of compounds per chunk.
		const auto chunk_size = max(1 + (cpdb.num_compounds - 1) / (num_threads << 2), num_hits);
		const auto num_chunks = 1 + (cpdb.num_compounds - 1) / chunk_size;
		assert(chunk_size * num_chunks >= cpdb.num_compounds);
		assert(chunk_size >= num_hits);
		cout << local_time_string() << "Using " << num_chunks << " chunks and a chunk size of " << chunk_size << endl;
		vector<size_t> scase(cpdb.num_compounds);
		vector<size_t> zcase(num_hits * (num_chunks - 1) + min(num_hits, cpdb.num_compounds - chunk_size * (num_chunks - 1))); // The last chunk might have fewer than num_hits records.
		ifstream conformers_sdf_ifs(cpdb.dpth / "conformers.sdf"); // ifstream conformers_sdf_ifs could be encapsulated in the compound_database class. It is purposely separated to maintain cpdb being constant.

		// Process each of the query compounds sequentially.
		ostringstream hit_mol_sdf_oss;
		const auto num_qry_mols = qry_mol_sup.length(); // num_qry_mols is the number of query molecules submitted by the user. These query molecules are not necessarily all processed, given the limitation of maximum 16MB MongoDB document size of the result.
		unsigned int query_number = 0;
		while (query_number < num_qry_mols)
		{
			cout << local_time_string() << "Parsing query compound " << query_number << endl;
			const unique_ptr<ROMol> qry_mol_ptr(qry_mol_sup.next()); // Calling next() may print "ERROR: Could not sanitize compound on line XXXX" to stderr.
			auto& qryMol = *qry_mol_ptr;

			// Get the number of atoms, including and excluding hydrogens.
			const auto num_atoms = qryMol.getNumAtoms();
			const auto num_heavy_atoms = qryMol.getNumHeavyAtoms();
			assert(num_heavy_atoms);
			cout << local_time_string() << "Found " << num_atoms << " atoms and " << num_heavy_atoms << " heavy atoms" << endl;

			// Calculate Morgan fingerprint.
			cout << local_time_string() << "Calculating Morgan fingerprint" << endl;
			const unique_ptr<SparseIntVect<uint32_t>> qryFp(getFingerprint(qryMol, 2));

			// Classify atoms to pharmacophoric subsets.
			cout << local_time_string() << "Classifying atoms into subsets" << endl;
			for (size_t k = 0; k < num_subsets; ++k)
			{
				vector<vector<pair<int, int>>> matchVect;
				SubstructMatch(qryMol, *SubsetMols[k], matchVect);
				const auto num_matches = matchVect.size();
				auto& subset = subsets[k];
				subset.resize(num_matches);
				for (size_t i = 0; i < num_matches; ++i)
				{
					subset[i] = matchVect[i].front().second;
				}
				cout << local_time_string() << "Found " << num_matches << " atoms for subset " << k << endl;
			}
			const auto& subset0 = subsets.front();
			assert(subset0.size() == num_heavy_atoms);

			// Calculate the four reference points.
			cout << local_time_string() << "Calculating " << num_refPoints << " reference points" << endl;
			const auto qryRefPoints = calcRefPoints(qryMol, subset0);
			const Point3DConstPtrVect qryRefPointv
			{{
				&qryRefPoints[0],
				&qryRefPoints[1],
				&qryRefPoints[2],
				&qryRefPoints[3],
			}};

			// Precalculate the distances of heavy atoms to the reference points, given that subsets[1 to 4] are subsets of subsets[0].
			cout << local_time_string() << "Calculating " << num_heavy_atoms * num_refPoints << " pairwise distances" << endl;
			const auto& qryCnf = qryMol.getConformer();
			for (size_t k = 0; k < num_refPoints; ++k)
			{
				const auto& refPoint = qryRefPoints[k];
				auto& distp = dista[k];
				distp.reserve(num_atoms);
				for (size_t i = 0; i < num_heavy_atoms; ++i)
				{
					distp[subset0[i]] = sqrt(dist2(qryCnf.getAtomPos(subset0[i]), refPoint));
				}
			}

			// Loop over pharmacophoric subsets and reference points.
			cout << local_time_string() << "Calculating " << 3 * num_refPoints * num_subsets << " moments of USRCAT feature" << endl;
			size_t qo = 0;
			for (const auto& subset : subsets)
			{
				const auto n = subset.size();
				for (size_t k = 0; k < num_refPoints; ++k)
				{
					// Load distances from precalculated ones.
					const auto& distp = dista[k];
					vector<double> dists(n);
					for (size_t i = 0; i < n; ++i)
					{
						dists[i] = distp[subset[i]];
					}

					// Compute moments.
					array<double, 3> m{};
					if (n > 2)
					{
						const auto v = 1.0 / n;
						for (size_t i = 0; i < n; ++i)
						{
							const auto d = dists[i];
							m[0] += d;
						}
						m[0] *= v;
						for (size_t i = 0; i < n; ++i)
						{
							const auto d = dists[i] - m[0];
							m[1] += d * d;
						}
						m[1] = sqrt(m[1] * v);
						for (size_t i = 0; i < n; ++i)
						{
							const auto d = dists[i] - m[0];
							m[2] += d * d * d;
						}
						m[2] = cbrt(m[2] * v);
					}
					else if (n == 2)
					{
						m[0] = 0.5 *     (dists[0] + dists[1]);
						m[1] = 0.5 * fabs(dists[0] - dists[1]);
					}
					else if (n == 1)
					{
						m[0] = dists[0];
					}
					for (const auto e : m)
					{
						q[qo++] = e;
					}
				}
			}
			assert(qo == qn.back());

			// Compute USR and USRCAT scores.
			cout << local_time_string() << "Screening " << cpdb.name << " and calculating " << cpdb.num_compounds << " " << usr_names[usr0] << " scores from " << cpdb.num_conformers << " conformers" << endl;
			scores.assign(scores.size(), numeric_limits<double>::max());
			iota(scase.begin(), scase.end(), 0);
			cnt.init(num_chunks);
			for (size_t l = 0; l < num_chunks; ++l)
			{
				io.post([&,l]()
				{
					// Loop over compounds of the current chunk.
					const auto chunk_beg = chunk_size * l;
					const auto chunk_end = min(chunk_beg + chunk_size, cpdb.num_compounds);
					for (size_t k = chunk_beg; k < chunk_end; ++k)
					{
						// Loop over conformers of the current compound and calculate their primary score.
						auto& scorek = scores[k];
						for (size_t j = k << 2; j < (k + 1) << 2; ++j)
						{
							const auto& d = cpdb.usrcat[j];
							double s = 0;
							for (size_t i = 0; i < qnu0; ++i)
							{
								s += abs(q[i] - d[i]);
								if (s >= scorek) break;
							}
							if (s < scorek)
							{
								scorek = s;
								cnfids[k] = j;
							}
						}
					}

					// Sort the scores of compounds of the current chunk.
					sort(scase.begin() + chunk_beg, scase.begin() + chunk_end, compare);

					// Copy the indexes of top hits of the current chunk to a global vector for final sorting.
					copy_n(scase.begin() + chunk_beg, min(num_hits, chunk_end - chunk_beg), zcase.begin() + num_hits * l);

					cnt.increment();
				});
			}
			cnt.wait();

			// Sort the top hits from chunks.
			cout << local_time_string() << "Sorting " << zcase.size() << " hits by " << usr_names[usr0] << " score" << endl;
			sort(zcase.begin(), zcase.end(), compare);

			// Write hit molecules to a string stream for output.
			// This loop can be parallelized, but beware of using independent conformers_sdf_ifs.
			// 1) array<ostringstream, num_hits> hit_mol_sdf_oss_arr; cnt.init(num_hits);
			// 2) for (size_t l = 0; l < num_hits; ++l) { io.post([&,l](){ SDWriter hit_mol_sdf_writer(&hit_mol_sdf_oss_arr[l], false); }); cnt.increment(); }
			// 3) cnt.wait(); ostringstream hit_mol_sdf_oss; for (size_t l = 0; l < num_hits; ++l) { hit_mol_sdf_oss << hit_mol_sdf_oss_arr[l]; } const auto hit_mol_sdf = hit_mol_sdf_oss.str();
			cout << local_time_string() << "Writing hit molecules to a string stream" << endl;
			ostringstream hit_mol_sdf_per_qry_oss;
			SDWriter hit_mol_sdf_writer(&hit_mol_sdf_per_qry_oss, false); // std::ostream*, bool takeOwnership
			for (size_t l = 0; l < num_hits; ++l)
			{
				// Obtain indexes to the hit compound and the hit conformer.
				const auto k = zcase[l];
				const auto j = cnfids[k];

				// Calculate the secondary score of the saved conformer, which has the best primary score.
				const auto& d = cpdb.usrcat[j];
				double s = 0;
				for (size_t i = 0; i < qnu1; ++i)
				{
					s += abs(q[i] - d[i]);
				}
				const auto u0score = 1 / (1 + scores[k] * qv[usr0]); // Primary score of the current compound.
				const auto u1score = 1 / (1 + s * qv[usr1]); // Secondary score of the current compound.

				// Read SDF content of the hit conformer.
				istringstream hit_mol_sdf_iss(cpdb.read_conformer(j, conformers_sdf_ifs));

				// Construct a RDKit ROMol object.
				SDMolSupplier hit_mol_sup(&hit_mol_sdf_iss, false, true, false, true);
				assert(hit_mol_sup.length() == 1);
				assert(hit_mol_sup.atEnd());
				const unique_ptr<ROMol> hit_mol_ptr(hit_mol_sup.next());
				auto& hitMol = *hit_mol_ptr;

				// Calculate Morgan fingerprint.
				const unique_ptr<SparseIntVect<uint32_t>> hitFp(getFingerprint(hitMol, 2));

				// Calculate Tanimoto similarity.
				const auto ts = TanimotoSimilarity(*qryFp, *hitFp);

				// Remove hydrogens to calculate canonical SMILES and descriptors.
				const unique_ptr<ROMol> hitMolNoH_ptr(removeHs(hitMol, false, false, false)); // implicitOnly, updateExplicitCount, sanitize
				const auto& hitMolNoH = *hitMolNoH_ptr;

				// Calculate canonical SMILES, molecular formula and descriptors. This can be done either by calculating them on the fly using the molecule with hydrogens removed, or by reading the precalculated values from *.u16 and *.f32 files.
				hitMol.setProp<unsigned int>("query", query_number);
				hitMol.setProp<double>("usrScore", usr1 ? u0score : u1score);
				hitMol.setProp<double>("usrcatScore", usr1 ? u1score : u0score);
				hitMol.setProp<double>("tanimotoScore", ts);
				hitMol.setProp<string>("database", cpdb.name);
				hitMol.setProp<string>("canonicalSMILES", MolToSmiles(hitMolNoH)); // Default parameters are: const ROMol& mol, bool doIsomericSmiles = true, bool doKekule = false, int rootedAtAtom = -1, bool canonical = true, bool allBondsExplicit = false, bool allHsExplicit = false, bool doRandom = false. https://www.rdkit.org/docs/cppapi/namespaceRDKit.html#a3636828cca83a233d7816f3652a9eb6b
				hitMol.setProp<string>("molFormula", calcMolFormula(hitMol)); // Calculate hydrogens in the molecular formula.
				hitMol.setProp<unsigned int>("numAtoms", cpdb.natm[k]);
				hitMol.setProp<unsigned int>("numHBD", cpdb.nhbd[k]);
				hitMol.setProp<unsigned int>("numHBA", cpdb.nhba[k]);
				hitMol.setProp<unsigned int>("numRotatableBonds", cpdb.nrtb[k]); // cpdb.nrtb[k] was precalculated from SMILES before adding hydrogens. Adding hydrogens may lead to more rotatable bonds. As a result, cpdb.nrtb[k] == calcNumRotatableBonds(hitMolNoH) <= calcNumRotatableBonds(hitMol)
				hitMol.setProp<unsigned int>("numRings", cpdb.nrng[k]);
				hitMol.setProp<double>("exactMW", cpdb.xmwt[k]);
				hitMol.setProp<double>("tPSA", cpdb.tpsa[k]);
				hitMol.setProp<double>("clogP", cpdb.clgp[k]);

				// Find heavy atoms.
				vector<vector<pair<int, int>>> matchVect;
				SubstructMatch(hitMol, *SubsetMols[0], matchVect);
				const auto num_matches = matchVect.size();
				assert(num_matches == hitMol.getNumHeavyAtoms());
				vector<int> hitHeavyAtoms(num_matches);
				for (size_t i = 0; i < num_matches; ++i)
				{
					hitHeavyAtoms[i] = matchVect[i].front().second;
//					assert(hitHeavyAtoms[i] == i); // Comment this assertion to avoid warning: comparison of integer expressions of different signedness. hitHeavyAtoms can be constructed using iota(hitHeavyAtoms.begin(), hitHeavyAtoms.end(), 0); because for RDKit-generated SDF compounds, heavy atom are always the first few atoms.
				}

				// Calculate the four reference points.
				const auto hitRefPoints = calcRefPoints(hitMol, hitHeavyAtoms);
				const Point3DConstPtrVect hitRefPointv
				{{
					&hitRefPoints[0],
					&hitRefPoints[1],
					&hitRefPoints[2],
					&hitRefPoints[3],
				}};

				// Calculate a 3D transform from the four reference points of the hit conformer to those of the query compound.
				Transform3D trans;
				AlignPoints(qryRefPointv, hitRefPointv, trans);

				// Apply the 3D transform to all atoms of the hit conformer.
				auto& hitCnf = hitMol.getConformer();
				transformConformer(hitCnf, trans);

				// Write the aligned hit conformer.
				hit_mol_sdf_writer.write(hitMol);
			}
			cout << local_time_string() << "Wrote " << hit_mol_sdf_per_qry_oss.tellp() << " bytes of hit molecules to a string stream" << endl;

			// If the size of the hitMolSdf field will not exceed 15MB after appending, then allow the appending. Reserve 1MB for the other fields, e.g. qryMolSdf.
			if (hit_mol_sdf_oss.tellp() + hit_mol_sdf_per_qry_oss.tellp() < 15000000)
			{
				hit_mol_sdf_oss << hit_mol_sdf_per_qry_oss.str();
				cout << local_time_string() << "Accumulated " << hit_mol_sdf_oss.tellp() << " bytes of hit molecules for the first " << ++query_number << " query molecules" << endl;
			}
			else
			{
				cout << local_time_string() << "Unable to accumulate " << hit_mol_sdf_per_qry_oss.tellp() << " bytes to the existing " << hit_mol_sdf_oss.tellp() << " bytes" << endl;
				break;
			}
		}
		const auto num_qry_mols_processed = query_number; // num_qry_mols_processed is the number of query molecules processed by the daemon.
		assert(num_qry_mols_processed <= num_qry_mols);
		cout << local_time_string() << "Processed " << num_qry_mols_processed << " out of " << num_qry_mols << " query molecules" << endl;

		// Update job status.
		const auto hit_mol_sdf = hit_mol_sdf_oss.str();
		cout << local_time_string() << "Writing " << hit_mol_sdf.size() << " bytes of hit molecules and setting end date" << endl;
		const auto endDate = system_clock::now();
		const int32_t cpdb_num_compounds = cpdb.num_compounds; // Create an int32_t intance, to be passed to kvp(). Caution: static_cast<int32_t>(cpdb.num_compounds) would cause the program to exit.
		const int32_t cpdb_num_conformers = cpdb.num_conformers;
		bsoncxx::builder::basic::document compt_update_builder;
		compt_update_builder.append(
			kvp("$set", [=](bsoncxx::builder::basic::sub_document set_subdoc) {
				set_subdoc.append(kvp("hitMolSdf", hit_mol_sdf));
				set_subdoc.append(kvp("endDate", bsoncxx::types::b_date(endDate)));
				set_subdoc.append(kvp("numQryMol", static_cast<int32_t>(num_qry_mols_processed))); // numQryMol is the number of query molecules completed by the daemon.
				set_subdoc.append(kvp("numLibMol", cpdb_num_compounds));
				set_subdoc.append(kvp("numLibCnf", cpdb_num_conformers));
			})
		);
		// http://mongocxx.org/api/current/classmongocxx_1_1collection.html#aece5216e5ae6fc3316c9da604f3b28f9
		const auto compt_update = coll.update_one(bsoncxx::builder::basic::make_document(kvp("_id", _id)), compt_update_builder.extract(), options::update()); // stdx::optional<result::update>. options: write_concern
		assert(compt_update);
		assert(compt_update->matched_count() == 1);
		assert(compt_update->modified_count() == 1);

		// Calculate runtime in seconds and screening speed in conformers per second.
		const auto runtime = duration_cast<nanoseconds>(endDate - startDate).count() * 1e-9; // Convert nanoseconds to seconds.
		const auto speed = cpdb.num_conformers * num_qry_mols_processed / runtime;
		cout
			<< local_time_string() << "Completed " << num_qry_mols_processed << " " << (num_qry_mols_processed == 1 ? "query" : "queries") << " in " << setprecision(3) << runtime << " seconds" << endl
			<< local_time_string() << "Screening speed was " << setprecision(0) << speed << " conformers per second" << endl
		;
	}
}

ext: cpp

max_stars_repo_name: HongjianLi/jstar-lbvs

max_issues_repo_name: HongjianLi/jstar-lbvs

max_forks_repo_name: HongjianLi/jstar-lbvs


text:
#include <boost/test/unit_test.hpp>

#include "geometry.h"
#include "logs.h"
#include "route.h"

using namespace GPS;

BOOST_AUTO_TEST_SUITE( Route_maxGradient_N0697611 )

const bool isFileName = true;

BOOST_AUTO_TEST_CASE( elev_zero_throughout )//checks that the output is 0 if all elevations are 0
{
   Route route = Route(LogFiles::GPXRoutesDir + "ABCD.gpx", isFileName);
   BOOST_CHECK_EQUAL( route.maxGradient(), 0 );
}

BOOST_AUTO_TEST_CASE( One_Point )//checks that the output is 0 when there is only 1 position
{
   Route route = Route(LogFiles::GPXRoutesDir + "N0697611_M_OnePoint.gpx", isFileName);
   BOOST_CHECK_EQUAL( route.maxGradient(), 0 );
}

BOOST_AUTO_TEST_CASE(  typical_flat)//Checks that a flat route still returns 0 at an elevation other than 0
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_ACE_CliftonCampus.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), 0,0.15) ;

}

BOOST_AUTO_TEST_CASE( typical_uphill)//Checks that a correct uphill gradient is calculated
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_CHM_CliftonCampus.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE(  typical_downhill)//Checks that a correct downhill gradient is calculated
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_MRW_CliftonCampus.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(-100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE( typical_uturn)//Checks that if two gradients of equal magnitude, but one positive and one negative, the positive one is the one returned
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_JIGQST_CliftonCampus.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

//
BOOST_AUTO_TEST_CASE( Flat_near_equator)//Checks that 0 is returned for a flat route
{                                         //this test is repeated at the equator
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_ACE_Pontianak.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), 0,0.15) ;

}

BOOST_AUTO_TEST_CASE( Equatorial_uphill)//Checks that a correct uphill gradient is calculated
{                                         //this test is repeated at the equator
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_CHM_Pontianak.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE( Equatorial_downhill)//Checks that a correct downhill gradient is calculated
{                                         //this test is repeated at the equator
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_MRW_Pontianak.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(-100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE( Equatorial_uturn)//Checks that if two gradients of equal magnitude, but one positive and one negative, the positive one is the one returned
{                                       //this test repeats this at the equator
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_JIGQST_Pontianak.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

//

BOOST_AUTO_TEST_CASE( EquatorialMeridian_uturn)//Checks that if two gradients of equal magnitude, but one positive and one negative, the positive one is the one returned
{                                               //This test repeats this at the equatorial meridian
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_JIGQST_EquatorialMeridian.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

//

BOOST_AUTO_TEST_CASE( EquatorialAntiMeridian_uturn)//Checks that if two gradients of equal magnitude, but one positive and one negative, the positive one is the one returned
{                                                   //this test repeats this at the equatorial anti meridian
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_JIGQST_EquatorialAntiMeridian.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

//

BOOST_AUTO_TEST_CASE( Northpole_flat)//Tests a flat route near the north pole
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_ACE_NorthPole.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), 0,0.15) ;

}
BOOST_AUTO_TEST_CASE( NorthPole_uphill)//Checks the gradient is accurate when going uphill near the northpole
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_CHM_NorthPole.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE( NorthPole_downhill)//Checks the gradient is accurate when going downhill near the northpole
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_MRW_NorthPole.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(-100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE( Flat_then_up) //checks that the correct gradient is returned when choosing between 0 and a positive gradient
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_VWXS_FlatThenUp.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(100,1000)),0.15) ;

}

BOOST_AUTO_TEST_CASE( Flat_then_down)//checks that the correct gradient is returned when choosing between 0 and a negative gradient
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_GHID_FlatThenDown.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), 0,0.15) ;

}

BOOST_AUTO_TEST_CASE( Steep_up) //checks that an extreme positive gradient is returned correctly
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_LM_Steepup.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(10000000,100)),0.15) ;

}

BOOST_AUTO_TEST_CASE( Steep_down)//checks that an extreme negative gradient is returned correctly
{
Route route = Route(LogFiles::GPXRoutesDir + "N0697611_MN_Steepdown.gpx", isFileName);

BOOST_CHECK_CLOSE(route.maxGradient(), radToDeg(atan2(-10000000,100)),0.15) ;

}

BOOST_AUTO_TEST_SUITE_END()

max_stars_repo_name: EdwinLangley/GPX-Route-and-Track

max_issues_repo_name: EdwinLangley/GPX-Route-and-Track

max_forks_repo_name: EdwinLangley/GPX-Route-and-Track