/*
    Access GPX data files.

    Copyright (C) 2002-2015 Robert Lipe, gpsbabel.org

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

 */

#include <cmath>                                   // for lround
#include <cstdio>                                  // for sscanf
#include <cstdlib>                                 // for atoi, strtod
#include <cstring>                                 // for strchr, strncpy

#include <QDate>                                   // for QDate
#include <QDateTime>                               // for QDateTime
#include <QHash>                                   // for QHash
#include <QIODevice>                               // for QIODevice, operator|, QIODevice::ReadOnly, QIODevice::Text, QIODevice::WriteOnly
#include <QLatin1Char>                             // for QLatin1Char
#include <QLatin1String>                           // for QLatin1String
#include <QString>                                 // for QString, QStringLiteral, operator+, operator==
#include <QStringList>                             // for QStringList
#include <QStringView>                             // for QStringView
#include <QTime>                                   // for QTime
#include <QVersionNumber>                          // for QVersionNumber
#include <QXmlStreamAttribute>                     // for QXmlStreamAttribute
#include <QXmlStreamAttributes>                    // for QXmlStreamAttributes
#include <QXmlStreamNamespaceDeclaration>          // for QXmlStreamNamespaceDeclaration
#include <QXmlStreamNamespaceDeclarations>         // for QXmlStreamNamespaceDeclarations
#include <QXmlStreamReader>                        // for QXmlStreamReader, QXmlStreamReader::Characters, QXmlStreamReader::EndDocument, QXmlStreamReader::EndElement, QXmlStreamReader::Invalid, QXmlStreamReader::StartElement
#include <Qt>                                      // for CaseInsensitive, UTC
#include <QtGlobal>                                // for qAsConst, QAddConst<>::Type

#include "defs.h"
#include "gpx.h"
#include "garmin_fs.h"                             // for garmin_fs_xml_convert, garmin_fs_xml_fprint, GMSD_FIND
#include "garmin_tables.h"                         // for gt_color_index_by_rgb, gt_color_name, gt_color_value_by_name
#include "src/core/datetime.h"                     // for DateTime
#include "src/core/file.h"                         // for File
#include "src/core/logging.h"                      // for Warning, Fatal
#include "src/core/xmlstreamwriter.h"              // for XmlStreamWriter
#include "src/core/xmltag.h"                       // for xml_tag, fs_xml, fs_xml_alloc, free_gpx_extras


#define MYNAME "GPX"
#ifndef CREATOR_NAME_URL
#  define CREATOR_NAME_URL "GPSBabel - https://www.gpsbabel.org"
#endif

void
GpxFormat::gpx_add_to_global(QStringList& ge, const QString& s)
{
  if (!ge.contains(s)) {
    ge.append(s);
  }
}

// Temporarily mock the old GPX writer's hardcoded fixed length for float/double
// types.  This can be removed once we have time/interest in regenerating all our
// zillion reference files.
inline QString GpxFormat::toString(double d)
{
  return QString::number(d, 'f', 9);
}

inline QString GpxFormat::toString(float f)
{
  return QString::number(f, 'f', 6);
}


/*
 * gpx_reset_short_handle: used for waypoint, route and track names
 * this allows gpx:wpt names to overlap gpx:rtept names, etc.
 */
void
GpxFormat::gpx_reset_short_handle()
{
  if (mkshort_handle != nullptr) {
    mkshort_del_handle(&mkshort_handle);
  }

  mkshort_handle = mkshort_new_handle();

  if (suppresswhite) {
    setshort_whitespace_ok(mkshort_handle, 0);
  }

  setshort_length(mkshort_handle, atoi(snlen));
}

void
GpxFormat::gpx_write_gdata(const QStringList& ge, const QString& tag) const
{
  if (!ge.isEmpty()) {
    writer->writeStartElement(tag);
    // TODO: This seems questionable.
    // We concatenate element content from multiple elements,
    // possibly from multiple input files, into one element.
    // This is necessary to comply with the schema as
    // these elements have maxOccurs limits of 1.
    for (const auto& str : ge) {
      writer->writeCharacters(str);
      /* Some tags we just output once. */
      if ((tag == QLatin1String("url")) ||
          (tag == QLatin1String("email"))) {
        break;
      }
    }
    writer->writeEndElement();
  }
}

GpxFormat::tag_mapping
GpxFormat::get_tag(const QString& t) const
{
  // returns default constructed value if key not found.
  return hash.value(t);
}

void
GpxFormat::tag_gpx(const QXmlStreamAttributes& attr)
{
  if (attr.hasAttribute(QLatin1String("version"))) {
    /* Set the default output version to the highest input
     * version.
     */
    QVersionNumber thisVersion = QVersionNumber::fromString(attr.value(QLatin1String("version")).toString()).normalized();
    if (gpx_highest_version_read.isNull()) {
      gpx_highest_version_read = thisVersion;
    } else if (!thisVersion.isNull() && (gpx_highest_version_read < thisVersion)) {
      gpx_highest_version_read = thisVersion;
    }
  }
  /* save namespace declarations in case we pass through elements
   * that use them to the writer.
   */
  const QXmlStreamNamespaceDeclarations ns = reader->namespaceDeclarations();
  for (const auto& n : ns) {
    QString prefix = n.prefix().toString();
    QString namespaceUri = n.namespaceUri().toString();
    /* don't toss any xsi declaration, it might used for tt_unknown or passthrough. */
    if (!prefix.isEmpty()) {
      if (! gpx_namespace_attribute.hasAttribute(prefix.prepend("xmlns:"))) {
        gpx_namespace_attribute.append(prefix, namespaceUri);
      }
    }
  }
}

void
GpxFormat::tag_wpt(const QXmlStreamAttributes& attr)
{
  wpt_tmp = new Waypoint;
  link_ = new UrlLink;

  cur_tag = nullptr;
  if (attr.hasAttribute(QLatin1String("lat"))) {
    wpt_tmp->latitude = attr.value(QLatin1String("lat")).toDouble();
  }
  if (attr.hasAttribute(QLatin1String("lon"))) {
    wpt_tmp->longitude = attr.value(QLatin1String("lon")).toDouble();
  }
  fs_ptr = &wpt_tmp->fs;
}

void
GpxFormat::tag_cache_desc(const QXmlStreamAttributes& attr)
{
  cache_descr_is_html = false;
  if (attr.hasAttribute(QLatin1String("html"))) {
    if (attr.value(QLatin1String("html")).compare(QLatin1String("True")) == 0) {
      cache_descr_is_html = true;
    }
  }
}

void
GpxFormat::tag_gs_cache(const QXmlStreamAttributes& attr) const
{
  geocache_data* gc_data = wpt_tmp->AllocGCData();

  if (attr.hasAttribute(QLatin1String("id"))) {
    gc_data->id = attr.value(QLatin1String(QLatin1String("id"))).toLongLong();
  }
  if (attr.hasAttribute(QLatin1String("available"))) {
    if (attr.value(QLatin1String("available")).compare(QLatin1String("True"), Qt::CaseInsensitive) == 0) {
      gc_data->is_available = status_true;
    } else if (attr.value(QLatin1String("available")).compare(QLatin1String("False"), Qt::CaseInsensitive) == 0) {
      gc_data->is_available = status_false;
    }
  }
  if (attr.hasAttribute(QLatin1String("archived"))) {
    if (attr.value(QLatin1String("archived")).compare(QLatin1String("True"), Qt::CaseInsensitive) == 0) {
      gc_data->is_archived = status_true;
    } else if (attr.value(QLatin1String("archived")).compare(QLatin1String("False"), Qt::CaseInsensitive) == 0) {
      gc_data->is_archived = status_false;
    }
  }
}

void
GpxFormat::start_something_else(QStringView el, const QXmlStreamAttributes& attr)
{
  if (!fs_ptr) {
    return;
  }

  auto* new_tag = new xml_tag;
  new_tag->tagname = el.toString();

  const QXmlStreamNamespaceDeclarations ns = reader->namespaceDeclarations();
  new_tag->attributes.reserve(attr.size() + ns.size());
  /*
   * It was found to be faster to append one element at a time compared to
   *   a) assiging new_tag->attributes = attr, or
   *   b) appending in one step, new_tag.attributes.apppend(attr)
   * Tested with on ubuntu bionic with Qt 5.9.5 and a large Geocache file
   * generated by Groundspeak.
   */
  for (const auto& a : attr) {
    new_tag->attributes.append(a);
  }
  for (const auto& n : ns) {
    QString prefix = n.prefix().toString().prepend(n.prefix().isEmpty()? "xmlns" : "xmlns:");
    QString namespaceUri = n.namespaceUri().toString();
    new_tag->attributes.append(prefix, namespaceUri);
  }

  if (cur_tag) {
    if (cur_tag->child) {
      cur_tag = cur_tag->child;
      while (cur_tag->sibling) {
        cur_tag = cur_tag->sibling;
      }
      cur_tag->sibling = new_tag;
      new_tag->parent = cur_tag->parent;
    } else {
      cur_tag->child = new_tag;
      new_tag->parent = cur_tag;
    }
  } else {
    const auto* fs_gpx = reinterpret_cast<fs_xml*>(fs_ptr->FsChainFind(kFsGpx));

    if (fs_gpx && fs_gpx->tag) {
      cur_tag = fs_gpx->tag;
      while (cur_tag->sibling) {
        cur_tag = cur_tag->sibling;
      }
      cur_tag->sibling = new_tag;
      new_tag->parent = nullptr;
    } else {
      fs_xml* new_fs_gpx = fs_xml_alloc(kFsGpx);
      new_fs_gpx->tag = new_tag;
      fs_ptr->FsChainAdd(new_fs_gpx);
      new_tag->parent = nullptr;
    }
  }
  cur_tag = new_tag;
}

void
GpxFormat::end_something_else()
{
  if (cur_tag) {
    cur_tag = cur_tag->parent;
  }
}

void
GpxFormat::tag_log_wpt(const QXmlStreamAttributes& attr) const
{
  /* create a new waypoint */
  auto* lwp_tmp = new Waypoint;

  /* extract the lat/lon attributes */
  if (attr.hasAttribute(QLatin1String("lat"))) {
    lwp_tmp->latitude = attr.value(QLatin1String("lat")).toDouble();
  }
  if (attr.hasAttribute(QLatin1String("lon"))) {
    lwp_tmp->longitude = attr.value(QLatin1String("lon")).toDouble();
  }
  /* Make a new shortname.  Since this is a groundspeak extension,
    we assume that GCBLAH is the current shortname format and that
    wpt_tmp refers to the currently parsed waypoint. Unfortunately,
    we need to keep track of log_wpt counts so we don't collide with
    dupe shortnames.
  */
  if (wpt_tmp->shortname.size() > 2) {
// FIXME: think harder about this later.
    lwp_tmp->shortname = wpt_tmp->shortname.mid(2, 4) + "-FIXME";

    waypt_add(lwp_tmp);
  }
}

void
GpxFormat::gpx_start(QStringView el, const QXmlStreamAttributes& attr)
{
  /*
   * Reset end-of-string without actually emptying/reallocing cdatastr.
   */
  cdatastr = QString();

  tag_mapping tag = get_tag(current_tag);
  switch (tag.type) {
  case tt_gpx:
    tag_gpx(attr);
    break;
  case tt_link:
    if (attr.hasAttribute(QLatin1String("href"))) {
      link_url = attr.value(QLatin1String("href")).toString();
    }
    break;
  case tt_wpt:
    tag_wpt(attr);
    break;
  case tt_wpttype_link:
    if (attr.hasAttribute(QLatin1String("href"))) {
      link_url = attr.value(QLatin1String("href")).toString();
    }
    break;
  case tt_rte:
    rte_head = new route_head;
    route_add_head(rte_head);
    rh_link_ = new UrlLink;
    fs_ptr = &rte_head->fs;
    break;
  case tt_rte_rtept:
    tag_wpt(attr);
    break;
  case tt_trk:
    trk_head = new route_head;
    track_add_head(trk_head);
    rh_link_ = new UrlLink;
    fs_ptr = &trk_head->fs;
    break;
  case tt_trk_trkseg_trkpt:
    tag_wpt(attr);
    if (next_trkpt_is_new_seg) {
      wpt_tmp->wpt_flags.new_trkseg = 1;
      next_trkpt_is_new_seg = 0;
    }
    break;
  case tt_rte_link:
  case tt_trk_link:
    if (attr.hasAttribute(QLatin1String("href"))) {
      link_url = attr.value(QLatin1String("href")).toString();
    }
    break;
  case tt_unknown:
    start_something_else(el, attr);
    return;
  case tt_cache:
    tag_gs_cache(attr);
    break;
  case tt_cache_log_wpt:
    if (opt_logpoint) {
      tag_log_wpt(attr);
    }
    break;
  case tt_cache_desc_long:
  case tt_cache_desc_short:
    tag_cache_desc(attr);
    break;
  case tt_cache_placer:
    if (attr.hasAttribute(QLatin1String("id"))) {
      wpt_tmp->AllocGCData()->placer_id = attr.value(QLatin1String("id")).toInt();
    }
  default:
    break;
  }
  if (tag.passthrough) {
    start_something_else(el, attr);
  }
}

struct
  gs_type_mapping {
  geocache_type type;
  const char* name;
} gs_type_map[] = {
  { gt_traditional, "Traditional Cache" },
  { gt_traditional, "Traditional" }, /* opencaching.de */
  { gt_multi, "Multi-cache" },
  { gt_multi, "Multi" }, /* opencaching.de */
  { gt_virtual, "Virtual Cache" },
  { gt_virtual, "Virtual" }, /* opencaching.de */
  { gt_event, "Event Cache" },
  { gt_event, "Event" }, /* opencaching.de */
  { gt_webcam, "Webcam Cache" },
  { gt_webcam, "Webcam" }, /* opencaching.de */
  { gt_surprise, "Unknown Cache" },
  { gt_earth, "Earthcache" },
  { gt_earth, "Earth" }, /* opencaching.de */
  { gt_cito, "Cache In Trash Out Event" },
  { gt_letterbox, "Letterbox Hybrid" },
  { gt_locationless, "Locationless (Reverse) Cache" },
  { gt_ape, "Project APE Cache" },
  { gt_mega, "Mega-Event Cache" },
  { gt_wherigo, "Wherigo Cache" },

  { gt_benchmark, "Benchmark" }, /* Not Groundspeak; for GSAK  */
};

struct
  gs_container_mapping {
  geocache_container type;
  const char* name;
} gs_container_map[] = {
  { gc_other, "Unknown" },
  { gc_other, "Other" }, /* Synonym on read. */
  { gc_micro, "Micro" },
  { gc_regular, "Regular" },
  { gc_large, "Large" },
  { gc_small, "Small" },
  { gc_virtual, "Virtual" }
};

geocache_type
gs_mktype(const QString& t)
{
  int sz = sizeof(gs_type_map) / sizeof(gs_type_map[0]);

  for (int i = 0; i < sz; i++) {
    if (!t.compare(gs_type_map[i].name,Qt::CaseInsensitive)) {
      return gs_type_map[i].type;
    }
  }
  return gt_unknown;
}

const char*
gs_get_cachetype(geocache_type t)
{
  int sz = sizeof(gs_type_map) / sizeof(gs_type_map[0]);

  for (int i = 0; i < sz; i++) {
    if (t == gs_type_map[i].type) {
      return gs_type_map[i].name;
    }
  }
  return "Unknown";
}

geocache_container
gs_mkcont(const QString& t)
{
  int sz = sizeof(gs_container_map) / sizeof(gs_container_map[0]);

  for (int i = 0; i < sz; i++) {
    if (!t.compare(gs_container_map[i].name,Qt::CaseInsensitive)) {
      return gs_container_map[i].type;
    }
  }
  return gc_unknown;
}

const char*
gs_get_container(geocache_container t)
{
  int sz = sizeof(gs_container_map) / sizeof(gs_container_map[0]);

  for (int i = 0; i < sz; i++) {
    if (t == gs_container_map[i].type) {
      return gs_container_map[i].name;
    }
  }
  return "Unknown";
}

gpsbabel::DateTime
xml_parse_time(const QString& dateTimeString)
{
  int off_hr = 0;
  int off_min = 0;
  int off_sign = 1;
  char* timestr = xstrdup(dateTimeString);

  char* offsetstr = strchr(timestr, 'Z');
  if (offsetstr) {
    /* zulu time; offsets stay at defaults */
    *offsetstr = '\0';
  } else {
    offsetstr = strchr(timestr, '+');
    if (offsetstr) {
      /* positive offset; parse it */
      *offsetstr = '\0';
      sscanf(offsetstr + 1, "%d:%d", &off_hr, &off_min);
    } else {
      offsetstr = strchr(timestr, 'T');
      if (offsetstr) {
        offsetstr = strchr(offsetstr, '-');
        if (offsetstr) {
          /* negative offset; parse it */
          *offsetstr = '\0';
          sscanf(offsetstr + 1, "%d:%d", &off_hr, &off_min);
          off_sign = -1;
        }
      }
    }
  }

  double fsec = 0;
  char* pointstr = strchr(timestr, '.');
  if (pointstr) {
    sscanf(pointstr, "%le", &fsec);
#if 0
    /* Round to avoid FP jitter */
    if (microsecs) {
      *microsecs = .5 + (fsec * 1000000.0) ;
    }
#endif
    *pointstr = '\0';
  }

  int year = 0, mon = 0, mday = 0, hour = 0, min = 0, sec = 0;
  QDateTime dt;
  int res = sscanf(timestr, "%d-%d-%dT%d:%d:%d", &year, &mon, &mday, &hour,
                   &min, &sec);
  if (res > 0) {
    QDate date(year, mon, mday);
    QTime time(hour, min, sec);
    dt = QDateTime(date, time, Qt::UTC);

    // Fractional part of time.
    if (fsec) {
      dt = dt.addMSecs(lround(fsec * 1000));
    }

    // Any offsets that were stuck at the end.
    dt = dt.addSecs(-off_sign * off_hr * 3600 - off_sign * off_min * 60);
  } else {
    dt = QDateTime();
  }
  xfree(timestr);
  return dt;
}

void
GpxFormat::gpx_end(QStringView /*unused*/)
{
  static QDateTime gc_log_date;

  // Remove leading, trailing whitespace.
  cdatastr = cdatastr.trimmed();

  tag_mapping tag = get_tag(current_tag);

  switch (tag.type) {
  /*
   * First, the tags that are file-global.
   */
  case tt_name:
    gpx_add_to_global(gpx_global->name, cdatastr);
    break;
  case tt_desc:
    gpx_add_to_global(gpx_global->desc, cdatastr);
    break;
  case tt_author:
    gpx_add_to_global(gpx_global->author, cdatastr);
    break;
  case tt_email:
    gpx_add_to_global(gpx_global->email, cdatastr);
    break;
  case tt_url:
    gpx_add_to_global(gpx_global->url, cdatastr);
    break;
  case tt_urlname:
    gpx_add_to_global(gpx_global->urlname, cdatastr);
    break;
  case tt_keywords:
    gpx_add_to_global(gpx_global->keywords, cdatastr);
    break;
  case tt_link:
    (gpx_global->link).AddUrlLink(UrlLink(link_url, link_text, link_type));
    link_type.clear();
    link_text.clear();
    link_url.clear();
    break;
  case tt_link_text:
    link_text = cdatastr;
    break;
  case tt_link_type:
    link_type = cdatastr;
    break;

  /*
   * Waypoint-specific tags.
   */
  case tt_wpt:
    if (link_) {
      if (!link_->url_.isEmpty()) {
        wpt_tmp->AddUrlLink(*link_);
      }
      delete link_;
      link_ = nullptr;
    }
    waypt_add(wpt_tmp);
    logpoint_ct = 0;
    cur_tag = nullptr;
    wpt_tmp = nullptr;
    break;
  case tt_cache_name:
    wpt_tmp->notes = cdatastr;
    break;
  case tt_cache_container:
    wpt_tmp->AllocGCData()->container = gs_mkcont(cdatastr);
    break;
  case tt_cache_type:
    wpt_tmp->AllocGCData()->type = gs_mktype(cdatastr);
    break;
  case tt_cache_difficulty:
    wpt_tmp->AllocGCData()->diff = cdatastr.toFloat() * 10;
    break;
  case tt_cache_hint:
    wpt_tmp->AllocGCData()->hint = cdatastr;
    break;
  case tt_cache_desc_long: {
    geocache_data* gc_data = wpt_tmp->AllocGCData();
    gc_data->desc_long.is_html = cache_descr_is_html;
    gc_data->desc_long.utfstring = cdatastr;
  }
  break;
  case tt_cache_desc_short: {
    geocache_data* gc_data = wpt_tmp->AllocGCData();
    gc_data->desc_short.is_html = cache_descr_is_html;
    gc_data->desc_short.utfstring = cdatastr;
  }
  break;
  case tt_cache_terrain:
    wpt_tmp->AllocGCData()->terr = cdatastr.toFloat() * 10;
    break;
  case tt_cache_placer:
    wpt_tmp->AllocGCData()->placer = cdatastr;
    break;
  case tt_cache_log_date:
    gc_log_date = xml_parse_time(cdatastr);
    break;
  /*
   * "Found it" logs follow the date according to the schema,
   * if this is the first "found it" for this waypt, just use the
   * last date we saw in this log.
   */
  case tt_cache_log_type:
    if ((cdatastr.compare(QLatin1String("Found it")) == 0) &&
        (0 == wpt_tmp->gc_data->last_found.toTime_t())) {
      wpt_tmp->AllocGCData()->last_found = gc_log_date;
    }
    gc_log_date = QDateTime();
    break;
  case tt_cache_favorite_points:
    wpt_tmp->AllocGCData()->favorite_points  = cdatastr.toInt();
    break;
  case tt_cache_personal_note:
    wpt_tmp->AllocGCData()->personal_note  = cdatastr;
    break;

  /*
   * Garmin-waypoint-specific tags.
   */
  case tt_garmin_wpt_proximity:
  case tt_garmin_wpt_temperature:
  case tt_garmin_wpt_depth:
  case tt_garmin_wpt_display_mode:
  case tt_garmin_wpt_category:
  case tt_garmin_wpt_addr:
  case tt_garmin_wpt_city:
  case tt_garmin_wpt_state:
  case tt_garmin_wpt_country:
  case tt_garmin_wpt_postal_code:
  case tt_garmin_wpt_phone_nr:
    garmin_fs_xml_convert(tt_garmin_wpt_extensions, tag.type, cdatastr, wpt_tmp);
    break;

  /*
   * Humminbird-waypoint-specific tags.
   */
  case tt_humminbird_wpt_depth:
  case tt_humminbird_trk_trkseg_trkpt_depth:
    WAYPT_SET(wpt_tmp, depth, cdatastr.toDouble() / 100.0);
    break;
  /*
   * Route-specific tags.
   */
  case tt_rte_name:
    rte_head->rte_name = cdatastr;
    break;
  case tt_rte:
    if (rh_link_) {
      if (!rh_link_->url_.isEmpty()) {
        rte_head->rte_urls.AddUrlLink(*rh_link_);
      }
      delete rh_link_;
      rh_link_ = nullptr;
    }
    break;
  case tt_rte_rtept:
    if (link_) {
      if (!link_->url_.isEmpty()) {
        wpt_tmp->AddUrlLink(*link_);
      }
      delete link_;
      link_ = nullptr;
    }
    route_add_wpt(rte_head, wpt_tmp);
    wpt_tmp = nullptr;
    break;
  case tt_rte_desc:
    rte_head->rte_desc = cdatastr;
    break;
  case tt_garmin_rte_display_color:
    rte_head->line_color.bbggrr = gt_color_value_by_name(cdatastr);
    break;
  case tt_rte_link:
    rte_head->rte_urls.AddUrlLink(UrlLink(link_url, link_text, link_type));
    link_type.clear();
    link_text.clear();
    link_url.clear();
    break;
  case tt_rte_number:
    rte_head->rte_num = cdatastr.toInt();
    break;
  /*
   * Track-specific tags.
   */
  case tt_trk_name:
    trk_head->rte_name = cdatastr;
    break;
  case tt_trk:
    if (rh_link_) {
      if (!rh_link_->url_.isEmpty()) {
        trk_head->rte_urls.AddUrlLink(*rh_link_);
      }
      delete rh_link_;
      rh_link_ = nullptr;
    }
    break;
  case tt_trk_trkseg:
    next_trkpt_is_new_seg = 1;
    break;
  case tt_trk_trkseg_trkpt:
    if (link_) {
      if (!link_->url_.isEmpty()) {
        wpt_tmp->AddUrlLink(*link_);
      }
      delete link_;
      link_ = nullptr;
    }
    track_add_wpt(trk_head, wpt_tmp);
    wpt_tmp = nullptr;
    break;
  case tt_trk_desc:
    trk_head->rte_desc = cdatastr;
    break;
  case tt_garmin_trk_display_color:
    trk_head->line_color.bbggrr = gt_color_value_by_name(cdatastr);
    break;
  case tt_trk_link:
    trk_head->rte_urls.AddUrlLink(UrlLink(link_url, link_text, link_type));
    link_type.clear();
    link_text.clear();
    link_url.clear();
    break;
  case tt_trk_number:
    trk_head->rte_num = cdatastr.toInt();
    break;
  case tt_trk_trkseg_trkpt_course:
    WAYPT_SET(wpt_tmp, course, cdatastr.toDouble());
    break;
  case tt_trk_trkseg_trkpt_speed:
    WAYPT_SET(wpt_tmp, speed, cdatastr.toDouble());
    break;
  case tt_trk_trkseg_trkpt_heartrate:
    wpt_tmp->heartrate = cdatastr.toDouble();
    break;
  case tt_trk_trkseg_trkpt_cadence:
    wpt_tmp->cadence = cdatastr.toDouble();
    break;

  /*
   * Items that are actually in multiple categories.
   */
  case tt_rte_url:
  case tt_trk_url:
    rh_link_->url_ = cdatastr;
    break;
  case tt_rte_urlname:
  case tt_trk_urlname:
    rh_link_->url_link_text_ = cdatastr;
    break;
  case tt_rte_link_text:
  case tt_trk_link_text:
    link_text = cdatastr;
    break;
  case tt_rte_link_type:
  case tt_trk_link_type:
    link_type = cdatastr;
    break;
  case tt_wpttype_ele:
    wpt_tmp->altitude = cdatastr.toDouble();
    break;
  case tt_wpttype_name:
    wpt_tmp->shortname = cdatastr;
    break;
  case tt_wpttype_sym:
    wpt_tmp->icon_descr = cdatastr;
    break;
  case tt_wpttype_time:
    wpt_tmp->SetCreationTime(xml_parse_time(cdatastr));
    break;
  case tt_wpttype_geoidheight:
    WAYPT_SET(wpt_tmp, geoidheight, cdatastr.toDouble());
    break;
  case tt_wpttype_cmt:
    wpt_tmp->description = cdatastr;
    break;
  case tt_wpttype_desc:
    wpt_tmp->notes = cdatastr;
    break;
  case tt_wpttype_pdop:
    wpt_tmp->pdop = cdatastr.toFloat();
    break;
  case tt_wpttype_hdop:
    wpt_tmp->hdop = cdatastr.toFloat();
    break;
  case tt_wpttype_vdop:
    wpt_tmp->vdop = cdatastr.toFloat();
    break;
  case tt_wpttype_sat:
    wpt_tmp->sat = cdatastr.toInt();
    break;
  case tt_wpttype_fix:
    if (cdatastr == QLatin1String("none")) {
      wpt_tmp->fix = fix_none;
    } else if (cdatastr == QLatin1String("2d")) {
      wpt_tmp->fix = fix_2d;
    } else if (cdatastr == QLatin1String("3d")) {
      wpt_tmp->fix = fix_3d;
    } else if (cdatastr == QLatin1String("dgps")) {
      wpt_tmp->fix = fix_dgps;
    } else if (cdatastr == QLatin1String("pps")) {
      wpt_tmp->fix = fix_pps;
    } else {
      wpt_tmp->fix = fix_unknown;
    }
    break;
  case tt_wpttype_url:
    link_->url_ = cdatastr;
    break;
  case tt_wpttype_urlname:
    link_->url_link_text_ = cdatastr;
    break;
  case tt_wpttype_link:
    waypt_add_url(wpt_tmp, link_url, link_text, link_type);
    link_type.clear();
    link_text.clear();
    link_url.clear();
    break;
  case tt_wpttype_link_text:
    link_text = cdatastr;
    break;
  case tt_wpttype_link_type:
    link_type = cdatastr;
    break;
  case tt_unknown:
    end_something_else();
    return;
  default:
    break;
  }

  if (tag.passthrough) {
    end_something_else();
  }

}


void
GpxFormat::gpx_cdata(QStringView s)
{
  QString* cdata;
  cdatastr += s.toString();

  if (!cur_tag) {
    return;
  }

  if (cur_tag->child) {
    xml_tag* tmp_tag = cur_tag->child;
    while (tmp_tag->sibling) {
      tmp_tag = tmp_tag->sibling;
    }
    cdata = &(tmp_tag->parentcdata);
  } else {
    cdata = &(cur_tag->cdata);
  }
  *cdata = cdatastr.trimmed();
}

void
GpxFormat::rd_init(const QString& fname)
{
  iqfile = new gpsbabel::File(fname);
  iqfile->open(QIODevice::ReadOnly);
  reader = new QXmlStreamReader(iqfile);

  current_tag.clear();

  cdatastr = QString();

  if (nullptr == gpx_global) {
    gpx_global = new GpxGlobal;
  }

  fs_ptr = nullptr;
}

void
GpxFormat::rd_deinit()
{
  delete reader;
  reader = nullptr;
  iqfile->close();
  delete iqfile;
  iqfile = nullptr;
  wpt_tmp = nullptr;
  cur_tag = nullptr;
}

void
GpxFormat::wr_init(const QString& fname)
{
  mkshort_handle = nullptr;
  oqfile = new gpsbabel::File(fname);
  oqfile->open(QIODevice::WriteOnly | QIODevice::Text);

  writer = new gpsbabel::XmlStreamWriter(oqfile);
  writer->setAutoFormattingIndent(2);
  writer->writeStartDocument();

  /* if an output version is not specified and an input version is
  * available use it, otherwise use the default.
  */

  if (opt_gpxver != nullptr) {
    gpx_write_version = QVersionNumber::fromString(opt_gpxver).normalized();
  } else if (!gpx_highest_version_read.isNull()) {
    gpx_write_version = gpx_highest_version_read;
  } else {
    gpx_write_version = gpx_1_0;
  }

  if (opt_humminbirdext || opt_garminext) {
    gpx_write_version = gpx_1_1;
  }

  // It's a good thing 0, 0.0, 0.0.0 aren't valid gpx versions,
  // normalization makes them null.
  if (gpx_write_version.isNull() || (gpx_write_version < gpx_1_0)) {
    fatal(FatalMsg() << MYNAME ": gpx version number"
            << gpx_write_version << "not valid.");
  }

  writer->setAutoFormatting(true);
  writer->writeStartElement(QStringLiteral("gpx"));
  writer->writeAttribute(QStringLiteral("version"),
                         QStringLiteral("%1.%2")
                         .arg(gpx_write_version.majorVersion())
                         .arg(gpx_write_version.minorVersion()));
  writer->writeAttribute(QStringLiteral("creator"), CREATOR_NAME_URL);
  writer->writeAttribute(QStringLiteral("xmlns"),
                         QStringLiteral("http://www.topografix.com/GPX/%1/%2")
                         .arg(gpx_write_version.majorVersion())
                         .arg(gpx_write_version.minorVersion()));
  if (opt_humminbirdext || opt_garminext) {
    if (opt_humminbirdext) {
      writer->writeAttribute(QStringLiteral("xmlns:h"), QStringLiteral("http://humminbird.com"));
    }
    if (opt_garminext) {
      writer->writeAttribute(QStringLiteral("xmlns:gpxx"),
                             QStringLiteral("http://www.garmin.com/xmlschemas/GpxExtensions/v3"));
      writer->writeAttribute(QStringLiteral("xmlns:gpxtpx"),
                             QStringLiteral("http://www.garmin.com/xmlschemas/TrackPointExtension/v1"));
    }
  } else {
    writer->writeAttributes(gpx_namespace_attribute);
  }

  if (gpx_write_version > gpx_1_0) {
    writer->writeStartElement(QStringLiteral("metadata"));
  }
  if (gpx_global) {
    gpx_write_gdata(gpx_global->name, QStringLiteral("name"));
    gpx_write_gdata(gpx_global->desc, QStringLiteral("desc"));
  }
  /* In GPX 1.1, author changed from a string to a PersonType.
   * since it's optional, we just drop it instead of rewriting it.
   */
  if (gpx_write_version < gpx_1_1) {
    if (gpx_global) {
      gpx_write_gdata(gpx_global->author, QStringLiteral("author"));
    }
  } // else {
  // TODO: gpx 1.1 author goes here.
  //}
  /* In GPX 1.1 email, url, urlname aren't allowed. */
  if (gpx_write_version < gpx_1_1) {
    if (gpx_global) {
      gpx_write_gdata(gpx_global->email, QStringLiteral("email"));
      gpx_write_gdata(gpx_global->url, QStringLiteral("url"));
      gpx_write_gdata(gpx_global->urlname, QStringLiteral("urlname"));
    }
  } else {
    if (gpx_global) {
      // TODO: gpx 1.1 copyright goes here
      for (const auto& l : qAsConst(gpx_global->link)) {
        writer->writeStartElement(QStringLiteral("link"));
        writer->writeAttribute(QStringLiteral("href"), l.url_);
        writer->writeOptionalTextElement(QStringLiteral("text"), l.url_link_text_);
        writer->writeOptionalTextElement(QStringLiteral("type"), l.url_link_type_);
        writer->writeEndElement();
      }
    }
  }

  gpsbabel::DateTime now = current_time();
  writer->writeTextElement(QStringLiteral("time"), now.toPrettyString());

  if (gpx_global) {
    gpx_write_gdata(gpx_global->keywords, QStringLiteral("keywords"));
  }

  gpx_write_bounds();

  // TODO: gpx 1.1 extensions go here.

  if (gpx_write_version > gpx_1_0) {
    writer->writeEndElement();
  }

}

void
GpxFormat::wr_deinit()
{
  writer->writeEndDocument();
  delete writer;
  writer = nullptr;
  oqfile->close();
  delete oqfile;
  oqfile = nullptr;

  mkshort_del_handle(&mkshort_handle);
}

void
GpxFormat::read()
{
  for (bool atEnd = false; !reader->atEnd() && !atEnd;)  {
    reader->readNext();
    // do processing
    switch (reader->tokenType()) {
    case QXmlStreamReader::StartElement:
      current_tag.append(QLatin1Char('/'));
      current_tag.append(reader->qualifiedName());
      gpx_start(reader->qualifiedName(), reader->attributes());
      break;

    case QXmlStreamReader::EndElement:
      gpx_end(reader->qualifiedName());
      current_tag.chop(reader->qualifiedName().length() + 1);
      cdatastr.clear();
      break;

    case QXmlStreamReader::Characters:
//    It is tempting to skip this if reader->isWhitespace().
//    That would lose all whitespace element values if the exist,
//    but it would skip line endings and indentation that doesn't matter.
      gpx_cdata(reader->text());
      break;

//  On windows with input redirection we can read an Invalid token
//  after the EndDocument token.  This also will set an error
//  "Premature end of document." that we will fatal on below.
//  This occurs with Qt 5.9.2 on windows when the file being
//  sent to stdin has dos line endings.
//  This does NOT occur with Qt 5.9.2 on windows when the file being
//  sent to stdin has unix line endings.
//  This does NOT occur with Qt 5.9.2 on windows with either line
//  endings if the file is read directly, i.e. not sent through stdin.
//  An example of a problematic file is reference/basecamp.gpx,
//  which fails on windows with this invocation from a command prompt:
//  .\GPSBabel.exe -i gpx -f - < reference\basecamp.gpx
//  This was demonstrated on 64 bit windows 10.  Other versions of
//  windows and Qt likely fail as well.
//  To avoid this we quit reading when we see the EndDocument.
//  This does not prevent us from correctly detecting the error
//  "Extra content at end of document."
    case QXmlStreamReader::EndDocument:
    case QXmlStreamReader::Invalid:
      atEnd = true;
      break;

    default:
      break;
    }
  }

  if (reader->hasError())  {
    fatal(FatalMsg() << MYNAME << "Read error:" << reader->errorString()
            << "File:" << iqfile->fileName()
            << "Line:" << reader->lineNumber()
            << "Column:" << reader->columnNumber());
  }
}

void
GpxFormat::write_attributes(const QXmlStreamAttributes& attributes) const
{
  for (const auto& attribute : attributes) {
    writer->writeAttribute(attribute.qualifiedName().toString(), attribute.value().toString());
  }
}

void
GpxFormat::fprint_xml_chain(xml_tag* tag, const Waypoint* wpt) const
{
  while (tag) {
    writer->writeStartElement(tag->tagname);
    write_attributes(tag->attributes);
    if (tag->cdata.isEmpty() && !tag->child) {
      // No children?  Self-closing tag.
      writer->writeEndElement();
    } else {
      if (!tag->cdata.isEmpty()) {
        writer->writeCharacters(tag->cdata);
      }
      if (tag->child) {
        fprint_xml_chain(tag->child, wpt);
      }
      if (wpt && wpt->gc_data->exported.isValid() &&
          tag->tagname.compare(QLatin1String("groundspeak:cache")) == 0) {
        writer->writeTextElement(QStringLiteral("time"),
                                 wpt->gc_data->exported.toPrettyString());
      }
      writer->writeEndElement();
    }
    if (!tag->parentcdata.isEmpty()) {
      // FIXME: The length check is necessary to get line endings correct in our test suite.
      // Writing the zero length string eats a newline, at least with Qt 4.6.2.
      writer->writeCharacters(tag->parentcdata);
    }
    tag = tag->sibling;
  }
}

/*
 * Handle the grossness of GPX 1.0 vs. 1.1 handling of linky links.
 */
void
GpxFormat::write_gpx_url(const UrlList& urls) const
{
  if (gpx_write_version > gpx_1_0) {
    for (const auto& l : urls) {
      if (!l.url_.isEmpty()) {
        writer->writeStartElement(QStringLiteral("link"));
        writer->writeAttribute(QStringLiteral("href"), l.url_);
        writer->writeOptionalTextElement(QStringLiteral("text"), l.url_link_text_);
        writer->writeOptionalTextElement(QStringLiteral("type"), l.url_link_type_);
        writer->writeEndElement();
      }
    }
  } else {
    UrlLink l = urls.GetUrlLink();
    if (!l.url_.isEmpty()) {
      writer->writeTextElement(QStringLiteral("url"), QString(urlbase) + l.url_);
      writer->writeOptionalTextElement(QStringLiteral("urlname"), l.url_link_text_);
    }
  }
}

void
GpxFormat::write_gpx_url(const Waypoint* waypointp) const
{
  if (waypointp->HasUrlLink()) {
    write_gpx_url(waypointp->urls);
  }
}

void
GpxFormat::write_gpx_url(const route_head* rh) const
{
  if (rh->rte_urls.HasUrlLink()) {
    write_gpx_url(rh->rte_urls);
  }
}

/*
 * Write optional accuracy information for a given (way|track|route)point
 * to the output stream.  Done in one place since it's common for all three.
 * Order counts.
 */
void
GpxFormat::gpx_write_common_acc(const Waypoint* waypointp) const
{
  const char* fix = nullptr;

  switch (waypointp->fix) {
  case fix_2d:
    fix = "2d";
    break;
  case fix_3d:
    fix = "3d";
    break;
  case fix_dgps:
    fix = "dgps";
    break;
  case fix_pps:
    fix = "pps";
    break;
  case fix_none:
    fix = "none";
    break;
  /* GPX spec says omit if we don't know. */
  case fix_unknown:
  default:
    break;
  }

  if (fix) {
    writer->writeTextElement(QStringLiteral("fix"), fix);
  }
  if (waypointp->sat > 0) {
    writer->writeTextElement(QStringLiteral("sat"), QString::number(waypointp->sat));
  }
  if (waypointp->hdop) {
    writer->writeTextElement(QStringLiteral("hdop"), toString(waypointp->hdop));
  }
  if (waypointp->vdop) {
    writer->writeTextElement(QStringLiteral("vdop"), toString(waypointp->vdop));
  }
  if (waypointp->pdop) {
    writer->writeTextElement(QStringLiteral("pdop"), toString(waypointp->pdop));
  }
  /* TODO: ageofdgpsdata should go here */
  /* TODO: dgpsid should go here */
}


void
GpxFormat::gpx_write_common_position(const Waypoint* waypointp, const gpx_point_type point_type) const
{
  if (waypointp->altitude != unknown_alt) {
    writer->writeTextElement(QStringLiteral("ele"), QString::number(waypointp->altitude, 'f', elevation_precision));
  }
  QString t = waypointp->CreationTimeXML();
  writer->writeOptionalTextElement(QStringLiteral("time"), t);
  if (gpxpt_track==point_type && gpx_1_0 == gpx_write_version) {
    /* These were accidentally removed from 1.1, and were only a part of trkpts in 1.0 */
    if WAYPT_HAS(waypointp, course) {
      writer->writeTextElement(QStringLiteral("course"), toString(waypointp->course));
    }
    if WAYPT_HAS(waypointp, speed) {
      writer->writeTextElement(QStringLiteral("speed"), toString(waypointp->speed));
    }
  }
  /* TODO:  magvar should go here */
  if (WAYPT_HAS(waypointp, geoidheight)) {
    writer->writeOptionalTextElement(QStringLiteral("geoidheight"),QString::number(waypointp->geoidheight, 'f', 1));
  }
}

void
GpxFormat::gpx_write_common_extensions(const Waypoint* waypointp, const gpx_point_type point_type) const
{
  // gpx version we are writing is >= 1.1.
  garmin_fs_t* gmsd = (opt_garminext) ? garmin_fs_t::find(waypointp) : nullptr;  // only needed if garmin extensions selected

  if ((opt_humminbirdext && (WAYPT_HAS(waypointp, depth) || WAYPT_HAS(waypointp, temperature))) ||
      (opt_garminext && gpxpt_route==point_type && gmsd != nullptr && gmsd->ilinks != nullptr)  ||
      (opt_garminext && gpxpt_waypoint==point_type && (WAYPT_HAS(waypointp, proximity) || WAYPT_HAS(waypointp, temperature) || WAYPT_HAS(waypointp, depth))) ||
      (opt_garminext && gpxpt_track==point_type && (WAYPT_HAS(waypointp, temperature) || WAYPT_HAS(waypointp, depth) || waypointp->heartrate != 0 || waypointp->cadence != 0))) {
    writer->writeStartElement(QStringLiteral("extensions"));

    if (opt_humminbirdext) {
      if (WAYPT_HAS(waypointp, depth)) {
        writer->writeTextElement(QStringLiteral("h:depth"), toString(waypointp->depth * 100.0));
      }
      if (WAYPT_HAS(waypointp, temperature)) {
        writer->writeTextElement(QStringLiteral("h:temperature"), toString(waypointp->temperature));
      }
    }

    if (opt_garminext) {
      // Although not required by the schema we assume that gpxx:WaypointExtension must be a child of gpx:wpt.
      // Although not required by the schema we assume that gpxx:RoutePointExtension must be a child of gpx:rtept.
      // Although not required by the schema we assume that gpxx:TrackPointExtension  must be a child of gpx:trkpt.
      // Although not required by the schema we assume that gpxtpx:TrackPointExtension must be a child of gpx:trkpt.
      switch (point_type) {
      case gpxpt_waypoint:
        if (WAYPT_HAS(waypointp, proximity) || WAYPT_HAS(waypointp, temperature) || WAYPT_HAS(waypointp, depth)) {
          writer->writeStartElement(QStringLiteral("gpxx:WaypointExtension"));
          if (WAYPT_HAS(waypointp, proximity)) {
            writer->writeTextElement(QStringLiteral("gpxx:Proximity"), toString(waypointp->proximity));
          }
          if (WAYPT_HAS(waypointp, temperature)) {
            writer->writeTextElement(QStringLiteral("gpxx:Temperature"), toString(waypointp->temperature));
          }
          if (WAYPT_HAS(waypointp, depth)) {
            writer->writeTextElement(QStringLiteral("gpxx:Depth"), toString(waypointp->depth));
          }
          writer->writeEndElement(); // "gpxx:WaypointExtension"
        }
        break;
      case gpxpt_route:
        if (gmsd != nullptr && gpxpt_route==point_type && gmsd->ilinks != nullptr) {
          writer->writeStartElement(QStringLiteral("gpxx:RoutePointExtension"));
          garmin_ilink_t* link = gmsd->ilinks;
          garmin_ilink_t* prior = nullptr;  // GDB files sometime contain repeated point; omit them
          while (link != nullptr) {
            if (prior == nullptr || prior->lat != link->lat || prior->lon != link->lon) {
              writer->writeStartElement(QStringLiteral("gpxx:rpt"));
              writer->writeAttribute(QStringLiteral("lat"), toString(link->lat));
              writer->writeAttribute(QStringLiteral("lon"), toString(link->lon));
              writer->writeEndElement(); // "gpxx:rpt"
            }
            prior = link;
            link = link->next;
          }
          writer->writeEndElement(); // "gpxx:RoutePointExtension"
        }
        break;
      case gpxpt_track:
        if (WAYPT_HAS(waypointp, temperature) || WAYPT_HAS(waypointp, depth) || waypointp->heartrate != 0 || waypointp->cadence != 0) {
          // gpxtpx:TrackPointExtension is a replacement for gpxx:TrackPointExtension.
          writer->writeStartElement(QStringLiteral("gpxtpx:TrackPointExtension"));
          if (WAYPT_HAS(waypointp, temperature)) {
            writer->writeTextElement(QStringLiteral("gpxtpx:atemp"), toString(waypointp->temperature));
          }
          if (WAYPT_HAS(waypointp, depth)) {
            writer->writeTextElement(QStringLiteral("gpxtpx:depth"), toString(waypointp->depth));
          }
          if (waypointp->heartrate != 0) {
            writer->writeTextElement(QStringLiteral("gpxtpx:hr"), QString::number(waypointp->heartrate));
          }
          if (waypointp->cadence != 0) {
            writer->writeTextElement(QStringLiteral("gpxtpx:cad"), QString::number(waypointp->cadence));
          }
          writer->writeEndElement(); // "gpxtpx:TrackPointExtension"
        }
        break;
      }
    }

    writer->writeEndElement(); // "extensions"
  }
}

void
GpxFormat::gpx_write_common_description(const Waypoint* waypointp, const QString& oname) const
{
  writer->writeOptionalTextElement(QStringLiteral("name"), oname);

  writer->writeOptionalTextElement(QStringLiteral("cmt"), waypointp->description);
  if (!waypointp->notes.isEmpty()) {
    writer->writeTextElement(QStringLiteral("desc"), waypointp->notes);
  } else {
    writer->writeOptionalTextElement(QStringLiteral("desc"), waypointp->description);
  }
  /* TODO: src should go here */
  write_gpx_url(waypointp);
  writer->writeOptionalTextElement(QStringLiteral("sym"), waypointp->icon_descr);
  /* TODO: type should go here */
}

void
GpxFormat::gpx_waypt_pr(const Waypoint* waypointp) const
{
  writer->writeStartElement(QStringLiteral("wpt"));
  writer->writeAttribute(QStringLiteral("lat"), toString(waypointp->latitude));
  writer->writeAttribute(QStringLiteral("lon"), toString(waypointp->longitude));

  QString oname = global_opts.synthesize_shortnames ?
                  mkshort_from_wpt(mkshort_handle, waypointp) :
                  waypointp->shortname;
  gpx_write_common_position(waypointp, gpxpt_waypoint);
  gpx_write_common_description(waypointp, oname);
  gpx_write_common_acc(waypointp);

  if (!(opt_humminbirdext || opt_garminext)) {
    const auto* fs_gpx = reinterpret_cast<fs_xml*>(waypointp->fs.FsChainFind(kFsGpx));
    auto* gmsd = garmin_fs_t::find(waypointp); /* gARmIN sPECIAL dATA */
    if (fs_gpx) {
      if (! gmsd) {
        fprint_xml_chain(fs_gpx->tag, waypointp);
      }
    }
    if (gmsd && (gpx_write_version > gpx_1_0)) {
      /* MapSource doesn't accepts extensions from 1.0 */
      garmin_fs_xml_fprint(waypointp, writer);
    }
  } else {
    gpx_write_common_extensions(waypointp, gpxpt_waypoint);
  }
  writer->writeEndElement();
}

void
GpxFormat::gpx_track_hdr(const route_head* rte)
{
  current_trk_head = rte;

  writer->writeStartElement(QStringLiteral("trk"));
  writer->writeOptionalTextElement(QStringLiteral("name"), rte->rte_name);
  writer->writeOptionalTextElement(QStringLiteral("desc"), rte->rte_desc);
  write_gpx_url(rte);

  if (rte->rte_num) {
    writer->writeTextElement(QStringLiteral("number"), QString::number(rte->rte_num));
  }

  if (gpx_write_version > gpx_1_0) {
    if (!(opt_humminbirdext || opt_garminext)) {
      const auto* fs_gpx = reinterpret_cast<fs_xml*>(rte->fs.FsChainFind(kFsGpx));
      if (fs_gpx) {
        fprint_xml_chain(fs_gpx->tag, nullptr);
      }
    } else if (opt_garminext) {
      if (rte->line_color.bbggrr > unknown_color) {
        int ci = gt_color_index_by_rgb(rte->line_color.bbggrr);
        if (ci > 0) {
          writer->writeStartElement(QStringLiteral("extensions"));
          writer->writeStartElement(QStringLiteral("gpxx:TrackExtension"));
          writer->writeTextElement(QStringLiteral("gpxx:DisplayColor"), QStringLiteral("%1")
                                   .arg(gt_color_name(ci)));
          writer->writeEndElement(); // Close gpxx:TrackExtension tag
          writer->writeEndElement(); // Close extensions tag
        }
      }
    }
  }
}

void
GpxFormat::gpx_track_disp(const Waypoint* waypointp) const
{
  bool first_in_trk = waypointp == current_trk_head->waypoint_list.front();

  if (waypointp->wpt_flags.new_trkseg) {
    if (!first_in_trk) {
      writer->writeEndElement();
    }
    writer->writeStartElement(QStringLiteral("trkseg"));
  }

  writer->writeStartElement(QStringLiteral("trkpt"));
  writer->writeAttribute(QStringLiteral("lat"), toString(waypointp->latitude));
  writer->writeAttribute(QStringLiteral("lon"), toString(waypointp->longitude));

  gpx_write_common_position(waypointp, gpxpt_track);

  QString oname = global_opts.synthesize_shortnames ?
                  mkshort_from_wpt(mkshort_handle, waypointp) :
                  waypointp->shortname;
  gpx_write_common_description(waypointp,
                               waypointp->wpt_flags.shortname_is_synthetic ?
                               nullptr : oname);
  gpx_write_common_acc(waypointp);

  if (!(opt_humminbirdext || opt_garminext)) {
    const auto* fs_gpx = reinterpret_cast<fs_xml*>(waypointp->fs.FsChainFind(kFsGpx));
    if (fs_gpx) {
      fprint_xml_chain(fs_gpx->tag, waypointp);
    }
  } else {
    gpx_write_common_extensions(waypointp, gpxpt_track);
  }
  writer->writeEndElement();
}

void
GpxFormat::gpx_track_tlr(const route_head* /*unused*/)
{
  if (!current_trk_head->waypoint_list.empty()) {
    writer->writeEndElement();
  }

  writer->writeEndElement();

  current_trk_head = nullptr;
}

void
GpxFormat::gpx_track_pr()
{
  auto gpx_track_hdr_lambda = [this](const route_head* rte)->void {
    gpx_track_hdr(rte);
  };
  auto gpx_track_tlr_lambda = [this](const route_head* rte)->void {
    gpx_track_tlr(rte);
  };
  auto gpx_track_disp_lambda = [this](const Waypoint* waypointp)->void {
    gpx_track_disp(waypointp);
  };
  track_disp_all(gpx_track_hdr_lambda, gpx_track_tlr_lambda, gpx_track_disp_lambda);
}

void
GpxFormat::gpx_route_hdr(const route_head* rte) const
{
  writer->writeStartElement(QStringLiteral("rte"));
  writer->writeOptionalTextElement(QStringLiteral("name"), rte->rte_name);
  writer->writeOptionalTextElement(QStringLiteral("desc"), rte->rte_desc);
  write_gpx_url(rte);

  if (rte->rte_num) {
    writer->writeTextElement(QStringLiteral("number"), QString::number(rte->rte_num));
  }

  if (gpx_write_version > gpx_1_0) {
    if (!(opt_humminbirdext || opt_garminext)) {
      const auto* fs_gpx = reinterpret_cast<fs_xml*>(rte->fs.FsChainFind(kFsGpx));
      if (fs_gpx) {
        fprint_xml_chain(fs_gpx->tag, nullptr);
      }
    } else if (opt_garminext) {
      if (rte->line_color.bbggrr > unknown_color) {
        int ci = gt_color_index_by_rgb(rte->line_color.bbggrr);
        if (ci > 0) {
          writer->writeStartElement(QStringLiteral("extensions"));
          writer->writeStartElement(QStringLiteral("gpxx:RouteExtension"));
          // FIXME: the value to use for IsAutoNamed is questionable.
          writer->writeTextElement(QStringLiteral("gpxx:IsAutoNamed"), rte->rte_name.isEmpty()? QStringLiteral("true") : QStringLiteral("false")); // Required element
          writer->writeTextElement(QStringLiteral("gpxx:DisplayColor"), QStringLiteral("%1")
                                   .arg(gt_color_name(ci)));
          writer->writeEndElement(); // Close gpxx:RouteExtension tag
          writer->writeEndElement(); // Close extensions tag
        }
      }
    }
  }
}

void
GpxFormat::gpx_route_disp(const Waypoint* waypointp) const
{
  writer->writeStartElement(QStringLiteral("rtept"));
  writer->writeAttribute(QStringLiteral("lat"), toString(waypointp->latitude));
  writer->writeAttribute(QStringLiteral("lon"), toString(waypointp->longitude));

  QString oname = global_opts.synthesize_shortnames ?
                  mkshort_from_wpt(mkshort_handle, waypointp) :
                  waypointp->shortname;
  gpx_write_common_position(waypointp, gpxpt_route);
  gpx_write_common_description(waypointp, oname);
  gpx_write_common_acc(waypointp);

  if (!(opt_humminbirdext || opt_garminext)) {
    const auto* fs_gpx = reinterpret_cast<fs_xml*>(waypointp->fs.FsChainFind(kFsGpx));
    if (fs_gpx) {
      fprint_xml_chain(fs_gpx->tag, waypointp);
    }
  } else {
    gpx_write_common_extensions(waypointp, gpxpt_route);
  }
  writer->writeEndElement();
}

void
GpxFormat::gpx_route_tlr(const route_head* /*unused*/) const
{
  writer->writeEndElement(); // Close rte tag.
}

void
GpxFormat::gpx_route_pr()
{
  /* output routes */
  auto gpx_route_hdr_lambda = [this](const route_head* rte)->void {
    gpx_route_hdr(rte);
  };
  auto gpx_route_tlr_lambda = [this](const route_head* rte)->void {
    gpx_route_tlr(rte);
  };
  auto gpx_route_disp_lambda = [this](const Waypoint* waypointp)->void {
    gpx_route_disp(waypointp);
  };
  route_disp_all(gpx_route_hdr_lambda, gpx_route_tlr_lambda, gpx_route_disp_lambda);
}

void
GpxFormat::gpx_waypt_bound_calc(const Waypoint* waypointp)
{
  waypt_add_to_bounds(&all_bounds, waypointp);
}

void
GpxFormat::gpx_write_bounds()
{
  waypt_init_bounds(&all_bounds);

  auto gpx_waypt_bound_calc_lambda = [this](const Waypoint* waypointp)->void {
    gpx_waypt_bound_calc(waypointp);
  };
  waypt_disp_all(gpx_waypt_bound_calc_lambda);
  route_disp_all(nullptr, nullptr, gpx_waypt_bound_calc_lambda);
  track_disp_all(nullptr, nullptr, gpx_waypt_bound_calc_lambda);

  if (waypt_bounds_valid(&all_bounds)) {
    writer->writeStartElement(QStringLiteral("bounds"));
    writer->writeAttribute(QStringLiteral("minlat"), toString(all_bounds.min_lat));
    writer->writeAttribute(QStringLiteral("minlon"), toString(all_bounds.min_lon));
    writer->writeAttribute(QStringLiteral("maxlat"), toString(all_bounds.max_lat));
    writer->writeAttribute(QStringLiteral("maxlon"), toString(all_bounds.max_lon));
    writer->writeEndElement();
  }
}

void
GpxFormat::write()
{

  elevation_precision = atoi(opt_elevation_precision);

  gpx_reset_short_handle();
  auto gpx_waypt_pr_lambda = [this](const Waypoint* waypointp)->void {
    gpx_waypt_pr(waypointp);
  };
  waypt_disp_all(gpx_waypt_pr_lambda);
  gpx_reset_short_handle();
  gpx_route_pr();
  gpx_reset_short_handle();
  gpx_track_pr();
  writer->writeEndElement(); // Close gpx tag.
}

void
GpxFormat::exit()
{
  gpx_highest_version_read = QVersionNumber();

  gpx_namespace_attribute.clear();

  delete gpx_global;
  gpx_global = nullptr;
}
