text: /******************************************************************************
   *
   * Project:  PROJ
   * Purpose:  ISO19111:2019 implementation
   * Author:   Even Rouault <even dot rouault at spatialys dot com>
   *
   ******************************************************************************
   * Copyright (c) 2018, Even Rouault <even dot rouault at spatialys dot com>
   *
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"),
   * to deal in the Software without restriction, including without limitation
   * the rights to use, copy, modify, merge, publish, distribute, sublicense,
   * and/or sell copies of the Software, and to permit persons to whom the
   * Software is furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included
   * in all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
   * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
   * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   * DEALINGS IN THE SOFTWARE.
   ****************************************************************************/
  
  #ifndef FROM_PROJ_CPP
  #define FROM_PROJ_CPP
  #endif
  
  #include "R-libproj/proj/metadata.hpp"
  #include "R-libproj/proj/common.hpp"
  #include "R-libproj/proj/io.hpp"
  #include "R-libproj/proj/util.hpp"
  
  #include "R-libproj/proj/internal/internal.hpp"
  #include "R-libproj/proj/internal/io_internal.hpp"
  
  #include "R-libproj/proj_json_streaming_writer.hpp"
  
  #include <algorithm>
  #include <memory>
  #include <string>
  #include <vector>
  
  using namespace NS_PROJ::internal;
  using namespace NS_PROJ::io;
  using namespace NS_PROJ::util;
  
  #if 0
  namespace dropbox{ namespace oxygen {
  template<> nn<std::shared_ptr<NS_PROJ::metadata::Citation>>::~nn() = default;
  template<> nn<NS_PROJ::metadata::ExtentPtr>::~nn() = default;
  template<> nn<NS_PROJ::metadata::GeographicBoundingBoxPtr>::~nn() = default;
  template<> nn<NS_PROJ::metadata::GeographicExtentPtr>::~nn() = default;
  template<> nn<NS_PROJ::metadata::VerticalExtentPtr>::~nn() = default;
  template<> nn<NS_PROJ::metadata::TemporalExtentPtr>::~nn() = default;
  template<> nn<NS_PROJ::metadata::IdentifierPtr>::~nn() = default;
  template<> nn<NS_PROJ::metadata::PositionalAccuracyPtr>::~nn() = default;
  }}
  #endif
  
  NS_PROJ_START
  namespace metadata {
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct Citation::Private {
      optional<std::string> title{};
  };
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  Citation::Citation() : d(internal::make_unique<Private>()) {}
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Constructs a citation by its title. */
  Citation::Citation(const std::string &titleIn)
      : d(internal::make_unique<Private>()) {
      d->title = titleIn;
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  Citation::Citation(const Citation &other)
      : d(internal::make_unique<Private>(*(other.d))) {}
  
  // ---------------------------------------------------------------------------
  
  Citation::~Citation() = default;
  
  // ---------------------------------------------------------------------------
  
  Citation &Citation::operator=(const Citation &other) {
      if (this != &other) {
          *d = *other.d;
      }
      return *this;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the name by which the cited resource is known. */
  const optional<std::string> &Citation::title() PROJ_PURE_DEFN {
      return d->title;
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct GeographicExtent::Private {};
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  GeographicExtent::GeographicExtent() : d(internal::make_unique<Private>()) {}
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  GeographicExtent::~GeographicExtent() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct GeographicBoundingBox::Private {
      double west_{};
      double south_{};
      double east_{};
      double north_{};
  
      Private(double west, double south, double east, double north)
          : west_(west), south_(south), east_(east), north_(north) {}
  
      bool intersects(const Private &other) const;
  
      std::unique_ptr<Private> intersection(const Private &other) const;
  };
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  GeographicBoundingBox::GeographicBoundingBox(double west, double south,
                                               double east, double north)
      : GeographicExtent(),
        d(internal::make_unique<Private>(west, south, east, north)) {}
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  GeographicBoundingBox::~GeographicBoundingBox() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the western-most coordinate of the limit of the dataset
   * extent.
   *
   * The unit is degrees.
   *
   * If eastBoundLongitude < westBoundLongitude(), then the bounding box crosses
   * the anti-meridian.
   */
  double GeographicBoundingBox::westBoundLongitude() PROJ_PURE_DEFN {
      return d->west_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the southern-most coordinate of the limit of the dataset
   * extent.
   *
   * The unit is degrees.
   */
  double GeographicBoundingBox::southBoundLatitude() PROJ_PURE_DEFN {
      return d->south_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the eastern-most coordinate of the limit of the dataset
   * extent.
   *
   * The unit is degrees.
   *
   * If eastBoundLongitude < westBoundLongitude(), then the bounding box crosses
   * the anti-meridian.
   */
  double GeographicBoundingBox::eastBoundLongitude() PROJ_PURE_DEFN {
      return d->east_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the northern-most coordinate of the limit of the dataset
   * extent.
   *
   * The unit is degrees.
   */
  double GeographicBoundingBox::northBoundLatitude() PROJ_PURE_DEFN {
      return d->north_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a GeographicBoundingBox.
   *
   * If east < west, then the bounding box crosses the anti-meridian.
   *
   * @param west Western-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param south Southern-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param east Eastern-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param north Northern-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @return a new GeographicBoundingBox.
   */
  GeographicBoundingBoxNNPtr GeographicBoundingBox::create(double west,
                                                           double south,
                                                           double east,
                                                           double north) {
      return GeographicBoundingBox::nn_make_shared<GeographicBoundingBox>(
          west, south, east, north);
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  bool GeographicBoundingBox::_isEquivalentTo(
      const util::IComparable *other, util::IComparable::Criterion,
      const io::DatabaseContextPtr &) const {
      auto otherExtent = dynamic_cast<const GeographicBoundingBox *>(other);
      if (!otherExtent)
          return false;
      return d->west_ == otherExtent->d->west_ &&
             d->south_ == otherExtent->d->south_ &&
             d->east_ == otherExtent->d->east_ &&
             d->north_ == otherExtent->d->north_;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  bool GeographicBoundingBox::contains(const GeographicExtentNNPtr &other) const {
      auto otherExtent = dynamic_cast<const GeographicBoundingBox *>(other.get());
      if (!otherExtent) {
          return false;
      }
      const double W = d->west_;
      const double E = d->east_;
      const double N = d->north_;
      const double S = d->south_;
      const double oW = otherExtent->d->west_;
      const double oE = otherExtent->d->east_;
      const double oN = otherExtent->d->north_;
      const double oS = otherExtent->d->south_;
  
      if (!(S <= oS && N >= oN)) {
          return false;
      }
  
      if (W == -180.0 && E == 180.0) {
          return true;
      }
  
      if (oW == -180.0 && oE == 180.0) {
          return false;
      }
  
      // Normal bounding box ?
      if (W < E) {
          if (oW < oE) {
              return W <= oW && E >= oE;
          } else {
              return false;
          }
          // No: crossing antimerian
      } else {
          if (oW < oE) {
              if (oW >= W) {
                  return true;
              } else if (oE <= E) {
                  return true;
              } else {
                  return false;
              }
          } else {
              return W <= oW && E >= oE;
          }
      }
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  bool GeographicBoundingBox::Private::intersects(const Private &other) const {
      const double W = west_;
      const double E = east_;
      const double N = north_;
      const double S = south_;
      const double oW = other.west_;
      const double oE = other.east_;
      const double oN = other.north_;
      const double oS = other.south_;
  
      if (N < oS || S > oN) {
          return false;
      }
  
      if (W == -180.0 && E == 180.0 && oW > oE) {
          return true;
      }
  
      if (oW == -180.0 && oE == 180.0 && W > E) {
          return true;
      }
  
      // Normal bounding box ?
      if (W <= E) {
          if (oW < oE) {
              if (std::max(W, oW) < std::min(E, oE)) {
                  return true;
              }
              return false;
          }
  
          return intersects(Private(oW, oS, 180.0, oN)) ||
                 intersects(Private(-180.0, oS, oE, oN));
  
          // No: crossing antimerian
      } else {
          if (oW <= oE) {
              return other.intersects(*this);
          }
  
          return true;
      }
  }
  //! @endcond
  
  bool GeographicBoundingBox::intersects(
      const GeographicExtentNNPtr &other) const {
      auto otherExtent = dynamic_cast<const GeographicBoundingBox *>(other.get());
      if (!otherExtent) {
          return false;
      }
      return d->intersects(*(otherExtent->d));
  }
  
  // ---------------------------------------------------------------------------
  
  GeographicExtentPtr
  GeographicBoundingBox::intersection(const GeographicExtentNNPtr &other) const {
      auto otherExtent = dynamic_cast<const GeographicBoundingBox *>(other.get());
      if (!otherExtent) {
          return nullptr;
      }
      auto ret = d->intersection(*(otherExtent->d));
      if (ret) {
          auto bbox = GeographicBoundingBox::create(ret->west_, ret->south_,
                                                    ret->east_, ret->north_);
          return bbox.as_nullable();
      }
      return nullptr;
  }
  
  //! @cond Doxygen_Suppress
  std::unique_ptr<GeographicBoundingBox::Private>
  GeographicBoundingBox::Private::intersection(const Private &otherExtent) const {
      const double W = west_;
      const double E = east_;
      const double N = north_;
      const double S = south_;
      const double oW = otherExtent.west_;
      const double oE = otherExtent.east_;
      const double oN = otherExtent.north_;
      const double oS = otherExtent.south_;
  
      if (N < oS || S > oN) {
          return nullptr;
      }
  
      if (W == -180.0 && E == 180.0 && oW > oE) {
          return internal::make_unique<Private>(oW, std::max(S, oS), oE,
                                                std::min(N, oN));
      }
  
      if (oW == -180.0 && oE == 180.0 && W > E) {
          return internal::make_unique<Private>(W, std::max(S, oS), E,
                                                std::min(N, oN));
      }
  
      // Normal bounding box ?
      if (W <= E) {
          if (oW < oE) {
              auto res = internal::make_unique<Private>(
                  std::max(W, oW), std::max(S, oS), std::min(E, oE),
                  std::min(N, oN));
              if (res->west_ < res->east_) {
                  return res;
              }
              return nullptr;
          }
  
          // Return larger of two parts of the multipolygon
          auto inter1 = intersection(Private(oW, oS, 180.0, oN));
          auto inter2 = intersection(Private(-180.0, oS, oE, oN));
          if (!inter1) {
              return inter2;
          }
          if (!inter2) {
              return inter1;
          }
          if (inter1->east_ - inter1->west_ > inter2->east_ - inter2->west_) {
              return inter1;
          }
          return inter2;
          // No: crossing antimerian
      } else {
          if (oW <= oE) {
              return otherExtent.intersection(*this);
          }
  
          return internal::make_unique<Private>(std::max(W, oW), std::max(S, oS),
                                                std::min(E, oE), std::min(N, oN));
      }
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct VerticalExtent::Private {
      double minimum_{};
      double maximum_{};
      common::UnitOfMeasureNNPtr unit_;
  
      Private(double minimum, double maximum,
              const common::UnitOfMeasureNNPtr &unit)
          : minimum_(minimum), maximum_(maximum), unit_(unit) {}
  };
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  VerticalExtent::VerticalExtent(double minimumIn, double maximumIn,
                                 const common::UnitOfMeasureNNPtr &unitIn)
      : d(internal::make_unique<Private>(minimumIn, maximumIn, unitIn)) {}
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  VerticalExtent::~VerticalExtent() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the minimum of the vertical extent.
   */
  double VerticalExtent::minimumValue() PROJ_PURE_DEFN { return d->minimum_; }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the maximum of the vertical extent.
   */
  double VerticalExtent::maximumValue() PROJ_PURE_DEFN { return d->maximum_; }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the unit of the vertical extent.
   */
  common::UnitOfMeasureNNPtr &VerticalExtent::unit() PROJ_PURE_DEFN {
      return d->unit_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a VerticalExtent.
   *
   * @param minimumIn minimum.
   * @param maximumIn maximum.
   * @param unitIn unit.
   * @return a new VerticalExtent.
   */
  VerticalExtentNNPtr
  VerticalExtent::create(double minimumIn, double maximumIn,
                         const common::UnitOfMeasureNNPtr &unitIn) {
      return VerticalExtent::nn_make_shared<VerticalExtent>(minimumIn, maximumIn,
                                                            unitIn);
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  bool VerticalExtent::_isEquivalentTo(const util::IComparable *other,
                                       util::IComparable::Criterion,
                                       const io::DatabaseContextPtr &) const {
      auto otherExtent = dynamic_cast<const VerticalExtent *>(other);
      if (!otherExtent)
          return false;
      return d->minimum_ == otherExtent->d->minimum_ &&
             d->maximum_ == otherExtent->d->maximum_ &&
             d->unit_ == otherExtent->d->unit_;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether this extent contains the other one.
   */
  bool VerticalExtent::contains(const VerticalExtentNNPtr &other) const {
      const double thisUnitToSI = d->unit_->conversionToSI();
      const double otherUnitToSI = other->d->unit_->conversionToSI();
      return d->minimum_ * thisUnitToSI <= other->d->minimum_ * otherUnitToSI &&
             d->maximum_ * thisUnitToSI >= other->d->maximum_ * otherUnitToSI;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether this extent intersects the other one.
   */
  bool VerticalExtent::intersects(const VerticalExtentNNPtr &other) const {
      const double thisUnitToSI = d->unit_->conversionToSI();
      const double otherUnitToSI = other->d->unit_->conversionToSI();
      return d->minimum_ * thisUnitToSI <= other->d->maximum_ * otherUnitToSI &&
             d->maximum_ * thisUnitToSI >= other->d->minimum_ * otherUnitToSI;
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct TemporalExtent::Private {
      std::string start_{};
      std::string stop_{};
  
      Private(const std::string &start, const std::string &stop)
          : start_(start), stop_(stop) {}
  };
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  TemporalExtent::TemporalExtent(const std::string &startIn,
                                 const std::string &stopIn)
      : d(internal::make_unique<Private>(startIn, stopIn)) {}
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  TemporalExtent::~TemporalExtent() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the start of the temporal extent.
   */
  const std::string &TemporalExtent::start() PROJ_PURE_DEFN { return d->start_; }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the end of the temporal extent.
   */
  const std::string &TemporalExtent::stop() PROJ_PURE_DEFN { return d->stop_; }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a TemporalExtent.
   *
   * @param start start.
   * @param stop stop.
   * @return a new TemporalExtent.
   */
  TemporalExtentNNPtr TemporalExtent::create(const std::string &start,
                                             const std::string &stop) {
      return TemporalExtent::nn_make_shared<TemporalExtent>(start, stop);
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  bool TemporalExtent::_isEquivalentTo(const util::IComparable *other,
                                       util::IComparable::Criterion,
                                       const io::DatabaseContextPtr &) const {
      auto otherExtent = dynamic_cast<const TemporalExtent *>(other);
      if (!otherExtent)
          return false;
      return start() == otherExtent->start() && stop() == otherExtent->stop();
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether this extent contains the other one.
   */
  bool TemporalExtent::contains(const TemporalExtentNNPtr &other) const {
      return start() <= other->start() && stop() >= other->stop();
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether this extent intersects the other one.
   */
  bool TemporalExtent::intersects(const TemporalExtentNNPtr &other) const {
      return start() <= other->stop() && stop() >= other->start();
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct Extent::Private {
      optional<std::string> description_{};
      std::vector<GeographicExtentNNPtr> geographicElements_{};
      std::vector<VerticalExtentNNPtr> verticalElements_{};
      std::vector<TemporalExtentNNPtr> temporalElements_{};
  };
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  Extent::Extent() : d(internal::make_unique<Private>()) {}
  
  // ---------------------------------------------------------------------------
  
  Extent::Extent(const Extent &other)
      : d(internal::make_unique<Private>(*other.d)) {}
  
  // ---------------------------------------------------------------------------
  
  Extent::~Extent() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** Return a textual description of the extent.
   *
   * @return the description, or empty.
   */
  const optional<std::string> &Extent::description() PROJ_PURE_DEFN {
      return d->description_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** Return the geographic element(s) of the extent
   *
   * @return the geographic element(s), or empty.
   */
  const std::vector<GeographicExtentNNPtr> &
  Extent::geographicElements() PROJ_PURE_DEFN {
      return d->geographicElements_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** Return the vertical element(s) of the extent
   *
   * @return the vertical element(s), or empty.
   */
  const std::vector<VerticalExtentNNPtr> &
  Extent::verticalElements() PROJ_PURE_DEFN {
      return d->verticalElements_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** Return the temporal element(s) of the extent
   *
   * @return the temporal element(s), or empty.
   */
  const std::vector<TemporalExtentNNPtr> &
  Extent::temporalElements() PROJ_PURE_DEFN {
      return d->temporalElements_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a Extent.
   *
   * @param descriptionIn Textual description, or empty.
   * @param geographicElementsIn Geographic element(s), or empty.
   * @param verticalElementsIn Vertical element(s), or empty.
   * @param temporalElementsIn Temporal element(s), or empty.
   * @return a new Extent.
   */
  ExtentNNPtr
  Extent::create(const optional<std::string> &descriptionIn,
                 const std::vector<GeographicExtentNNPtr> &geographicElementsIn,
                 const std::vector<VerticalExtentNNPtr> &verticalElementsIn,
                 const std::vector<TemporalExtentNNPtr> &temporalElementsIn) {
      auto extent = Extent::nn_make_shared<Extent>();
      extent->assignSelf(extent);
      extent->d->description_ = descriptionIn;
      extent->d->geographicElements_ = geographicElementsIn;
      extent->d->verticalElements_ = verticalElementsIn;
      extent->d->temporalElements_ = temporalElementsIn;
      return extent;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a Extent from a bounding box
   *
   * @param west Western-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param south Southern-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param east Eastern-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param north Northern-most coordinate of the limit of the dataset extent (in
   * degrees).
   * @param descriptionIn Textual description, or empty.
   * @return a new Extent.
   */
  ExtentNNPtr
  Extent::createFromBBOX(double west, double south, double east, double north,
                         const util::optional<std::string> &descriptionIn) {
      return create(
          descriptionIn,
          std::vector<GeographicExtentNNPtr>{
              nn_static_pointer_cast<GeographicExtent>(
                  GeographicBoundingBox::create(west, south, east, north))},
          std::vector<VerticalExtentNNPtr>(), std::vector<TemporalExtentNNPtr>());
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  bool Extent::_isEquivalentTo(const util::IComparable *other,
                               util::IComparable::Criterion criterion,
                               const io::DatabaseContextPtr &dbContext) const {
      auto otherExtent = dynamic_cast<const Extent *>(other);
      bool ret =
          (otherExtent &&
           description().has_value() == otherExtent->description().has_value() &&
           *description() == *otherExtent->description() &&
           d->geographicElements_.size() ==
               otherExtent->d->geographicElements_.size() &&
           d->verticalElements_.size() ==
               otherExtent->d->verticalElements_.size() &&
           d->temporalElements_.size() ==
               otherExtent->d->temporalElements_.size());
      if (ret) {
          for (size_t i = 0; ret && i < d->geographicElements_.size(); ++i) {
              ret = d->geographicElements_[i]->_isEquivalentTo(
                  otherExtent->d->geographicElements_[i].get(), criterion,
                  dbContext);
          }
          for (size_t i = 0; ret && i < d->verticalElements_.size(); ++i) {
              ret = d->verticalElements_[i]->_isEquivalentTo(
                  otherExtent->d->verticalElements_[i].get(), criterion,
                  dbContext);
          }
          for (size_t i = 0; ret && i < d->temporalElements_.size(); ++i) {
              ret = d->temporalElements_[i]->_isEquivalentTo(
                  otherExtent->d->temporalElements_[i].get(), criterion,
                  dbContext);
          }
      }
      return ret;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether this extent contains the other one.
   *
   * Behavior only well specified if each sub-extent category as at most
   * one element.
   */
  bool Extent::contains(const ExtentNNPtr &other) const {
      bool res = true;
      if (d->geographicElements_.size() == 1 &&
          other->d->geographicElements_.size() == 1) {
          res = d->geographicElements_[0]->contains(
              other->d->geographicElements_[0]);
      }
      if (res && d->verticalElements_.size() == 1 &&
          other->d->verticalElements_.size() == 1) {
          res = d->verticalElements_[0]->contains(other->d->verticalElements_[0]);
      }
      if (res && d->temporalElements_.size() == 1 &&
          other->d->temporalElements_.size() == 1) {
          res = d->temporalElements_[0]->contains(other->d->temporalElements_[0]);
      }
      return res;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether this extent intersects the other one.
   *
   * Behavior only well specified if each sub-extent category as at most
   * one element.
   */
  bool Extent::intersects(const ExtentNNPtr &other) const {
      bool res = true;
      if (d->geographicElements_.size() == 1 &&
          other->d->geographicElements_.size() == 1) {
          res = d->geographicElements_[0]->intersects(
              other->d->geographicElements_[0]);
      }
      if (res && d->verticalElements_.size() == 1 &&
          other->d->verticalElements_.size() == 1) {
          res =
              d->verticalElements_[0]->intersects(other->d->verticalElements_[0]);
      }
      if (res && d->temporalElements_.size() == 1 &&
          other->d->temporalElements_.size() == 1) {
          res =
              d->temporalElements_[0]->intersects(other->d->temporalElements_[0]);
      }
      return res;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns the intersection of this extent with another one.
   *
   * Behavior only well specified if there is one single GeographicExtent
   * in each object.
   * Returns nullptr otherwise.
   */
  ExtentPtr Extent::intersection(const ExtentNNPtr &other) const {
      if (d->geographicElements_.size() == 1 &&
          other->d->geographicElements_.size() == 1) {
          if (contains(other)) {
              return other.as_nullable();
          }
          auto self = util::nn_static_pointer_cast<Extent>(shared_from_this());
          if (other->contains(self)) {
              return self.as_nullable();
          }
          auto geogIntersection = d->geographicElements_[0]->intersection(
              other->d->geographicElements_[0]);
          if (geogIntersection) {
              return create(util::optional<std::string>(),
                            std::vector<GeographicExtentNNPtr>{
                                NN_NO_CHECK(geogIntersection)},
                            std::vector<VerticalExtentNNPtr>{},
                            std::vector<TemporalExtentNNPtr>{});
          }
      }
      return nullptr;
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct Identifier::Private {
      optional<Citation> authority_{};
      std::string code_{};
      optional<std::string> codeSpace_{};
      optional<std::string> version_{};
      optional<std::string> description_{};
      optional<std::string> uri_{};
  
      Private() = default;
  
      Private(const std::string &codeIn, const PropertyMap &properties)
          : code_(codeIn) {
          setProperties(properties);
      }
  
    private:
      // cppcheck-suppress functionStatic
      void setProperties(const PropertyMap &properties);
  };
  
  // ---------------------------------------------------------------------------
  
  void Identifier::Private::setProperties(
      const PropertyMap &properties) // throw(InvalidValueTypeException)
  {
      {
          const auto pVal = properties.get(AUTHORITY_KEY);
          if (pVal) {
              if (auto genVal = dynamic_cast<const BoxedValue *>(pVal->get())) {
                  if (genVal->type() == BoxedValue::Type::STRING) {
                      authority_ = Citation(genVal->stringValue());
                  } else {
                      throw InvalidValueTypeException("Invalid value type for " +
                                                      AUTHORITY_KEY);
                  }
              } else {
                  if (auto citation =
                          dynamic_cast<const Citation *>(pVal->get())) {
                      authority_ = *citation;
                  } else {
                      throw InvalidValueTypeException("Invalid value type for " +
                                                      AUTHORITY_KEY);
                  }
              }
          }
      }
  
      {
          const auto pVal = properties.get(CODE_KEY);
          if (pVal) {
              if (auto genVal = dynamic_cast<const BoxedValue *>(pVal->get())) {
                  if (genVal->type() == BoxedValue::Type::INTEGER) {
                      code_ = toString(genVal->integerValue());
                  } else if (genVal->type() == BoxedValue::Type::STRING) {
                      code_ = genVal->stringValue();
                  } else {
                      throw InvalidValueTypeException("Invalid value type for " +
                                                      CODE_KEY);
                  }
              } else {
                  throw InvalidValueTypeException("Invalid value type for " +
                                                  CODE_KEY);
              }
          }
      }
  
      properties.getStringValue(CODESPACE_KEY, codeSpace_);
      properties.getStringValue(VERSION_KEY, version_);
      properties.getStringValue(DESCRIPTION_KEY, description_);
      properties.getStringValue(URI_KEY, uri_);
  }
  
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  Identifier::Identifier(const std::string &codeIn,
                         const util::PropertyMap &properties)
      : d(internal::make_unique<Private>(codeIn, properties)) {}
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  
  // ---------------------------------------------------------------------------
  
  Identifier::Identifier() : d(internal::make_unique<Private>()) {}
  
  // ---------------------------------------------------------------------------
  
  Identifier::Identifier(const Identifier &other)
      : d(internal::make_unique<Private>(*(other.d))) {}
  
  // ---------------------------------------------------------------------------
  
  Identifier::~Identifier() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a Identifier.
   *
   * @param codeIn Alphanumeric value identifying an instance in the codespace
   * @param properties See \ref general_properties.
   * Generally, the Identifier::CODESPACE_KEY should be set.
   * @return a new Identifier.
   */
  IdentifierNNPtr Identifier::create(const std::string &codeIn,
                                     const PropertyMap &properties) {
      return Identifier::nn_make_shared<Identifier>(codeIn, properties);
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  IdentifierNNPtr
  Identifier::createFromDescription(const std::string &descriptionIn) {
      auto id = Identifier::nn_make_shared<Identifier>();
      id->d->description_ = descriptionIn;
      return id;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return a citation for the organization responsible for definition and
   * maintenance of the code.
   *
   * @return the citation for the authority, or empty.
   */
  const optional<Citation> &Identifier::authority() PROJ_PURE_DEFN {
      return d->authority_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return the alphanumeric value identifying an instance in the
   * codespace.
   *
   * e.g. "4326" (for EPSG:4326 WGS 84 GeographicCRS)
   *
   * @return the code.
   */
  const std::string &Identifier::code() PROJ_PURE_DEFN { return d->code_; }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return the organization responsible for definition and maintenance of
   * the code.
   *
   * e.g "EPSG"
   *
   * @return the authority codespace, or empty.
   */
  const optional<std::string> &Identifier::codeSpace() PROJ_PURE_DEFN {
      return d->codeSpace_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return the version identifier for the namespace.
   *
   * When appropriate, the edition is identified by the effective date, coded
   * using ISO 8601 date format.
   *
   * @return the version or empty.
   */
  const optional<std::string> &Identifier::version() PROJ_PURE_DEFN {
      return d->version_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return the natural language description of the meaning of the code
   * value.
   *
   * @return the description or empty.
   */
  const optional<std::string> &Identifier::description() PROJ_PURE_DEFN {
      return d->description_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return the URI of the identifier.
   *
   * @return the URI or empty.
   */
  const optional<std::string> &Identifier::uri() PROJ_PURE_DEFN {
      return d->uri_;
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  void Identifier::_exportToWKT(WKTFormatter *formatter) const {
      const bool isWKT2 = formatter->version() == WKTFormatter::Version::WKT2;
      const std::string &l_code = code();
      const std::string &l_codeSpace = *codeSpace();
      if (!l_codeSpace.empty() && !l_code.empty()) {
          if (isWKT2) {
              formatter->startNode(WKTConstants::ID, false);
              formatter->addQuotedString(l_codeSpace);
              try {
                  (void)std::stoi(l_code);
                  formatter->add(l_code);
              } catch (const std::exception &) {
                  formatter->addQuotedString(l_code);
              }
              if (version().has_value()) {
                  auto l_version = *(version());
                  try {
                      (void)c_locale_stod(l_version);
                      formatter->add(l_version);
                  } catch (const std::exception &) {
                      formatter->addQuotedString(l_version);
                  }
              }
              if (authority().has_value() &&
                  *(authority()->title()) != l_codeSpace) {
                  formatter->startNode(WKTConstants::CITATION, false);
                  formatter->addQuotedString(*(authority()->title()));
                  formatter->endNode();
              }
              if (uri().has_value()) {
                  formatter->startNode(WKTConstants::URI, false);
                  formatter->addQuotedString(*(uri()));
                  formatter->endNode();
              }
              formatter->endNode();
          } else {
              formatter->startNode(WKTConstants::AUTHORITY, false);
              formatter->addQuotedString(l_codeSpace);
              formatter->addQuotedString(l_code);
              formatter->endNode();
          }
      }
  }
  
  // ---------------------------------------------------------------------------
  
  void Identifier::_exportToJSON(JSONFormatter *formatter) const {
      const std::string &l_code = code();
      const std::string &l_codeSpace = *codeSpace();
      if (!l_codeSpace.empty() && !l_code.empty()) {
          auto writer = formatter->writer();
          auto objContext(formatter->MakeObjectContext(nullptr, false));
          writer->AddObjKey("authority");
          writer->Add(l_codeSpace);
          writer->AddObjKey("code");
          try {
              writer->Add(std::stoi(l_code));
          } catch (const std::exception &) {
              writer->Add(l_code);
          }
      }
  }
  
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  static bool isIgnoredChar(char ch) {
      return ch == ' ' || ch == '_' || ch == '-' || ch == '/' || ch == '(' ||
             ch == ')' || ch == '.' || ch == '&' || ch == ',';
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  static const struct utf8_to_lower {
      const char *utf8;
      char ascii;
  } map_utf8_to_lower[] = {
      {"\xc3\xa1", 'a'}, // a acute
      {"\xc3\xa4", 'a'}, // a tremma
  
      {"\xc4\x9b", 'e'}, // e reverse circumflex
      {"\xc3\xa8", 'e'}, // e grave
      {"\xc3\xa9", 'e'}, // e acute
      {"\xc3\xab", 'e'}, // e tremma
  
      {"\xc3\xad", 'i'}, // i grave
  
      {"\xc3\xb4", 'o'}, // o circumflex
      {"\xc3\xb6", 'o'}, // o tremma
  
      {"\xc3\xa7", 'c'}, // c cedilla
  };
  
  static const struct utf8_to_lower *get_ascii_replacement(const char *c_str) {
      for (const auto &pair : map_utf8_to_lower) {
          if (*c_str == pair.utf8[0] &&
              strncmp(c_str, pair.utf8, strlen(pair.utf8)) == 0) {
              return &pair;
          }
      }
      return nullptr;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  std::string Identifier::canonicalizeName(const std::string &str) {
      std::string res;
      const char *c_str = str.c_str();
      for (size_t i = 0; c_str[i] != 0; ++i) {
          const auto ch = c_str[i];
          if (ch == ' ' && c_str[i + 1] == '+' && c_str[i + 2] == ' ') {
              i += 2;
              continue;
          }
          if (ch == '1' && !res.empty() &&
              !(res.back() >= '0' && res.back() <= '9') && c_str[i + 1] == '9' &&
              c_str[i + 2] >= '0' && c_str[i + 2] <= '9') {
              ++i;
              continue;
          }
          if (static_cast<unsigned char>(ch) > 127) {
              const auto *replacement = get_ascii_replacement(c_str + i);
              if (replacement) {
                  res.push_back(replacement->ascii);
                  i += strlen(replacement->utf8) - 1;
                  continue;
              }
          }
          if (!isIgnoredChar(ch)) {
              res.push_back(static_cast<char>(::tolower(ch)));
          }
      }
      return res;
  }
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Returns whether two names are considered equivalent.
   *
   * Two names are equivalent by removing any space, underscore, dash, slash,
   * { or } character from them, and comparing in a case insensitive way.
   */
  bool Identifier::isEquivalentName(const char *a, const char *b) noexcept {
      size_t i = 0;
      size_t j = 0;
      char lastValidA = 0;
      char lastValidB = 0;
      while (a[i] != 0 && b[j] != 0) {
          char aCh = a[i];
          char bCh = b[j];
          if (aCh == ' ' && a[i + 1] == '+' && a[i + 2] == ' ') {
              i += 3;
              continue;
          }
          if (bCh == ' ' && b[j + 1] == '+' && b[j + 2] == ' ') {
              j += 3;
              continue;
          }
          if (isIgnoredChar(aCh)) {
              ++i;
              continue;
          }
          if (isIgnoredChar(bCh)) {
              ++j;
              continue;
          }
          if (aCh == '1' && !(lastValidA >= '0' && lastValidA <= '9') &&
              a[i + 1] == '9' && a[i + 2] >= '0' && a[i + 2] <= '9') {
              i += 2;
              lastValidA = '9';
              continue;
          }
          if (bCh == '1' && !(lastValidB >= '0' && lastValidB <= '9') &&
              b[j + 1] == '9' && b[j + 2] >= '0' && b[j + 2] <= '9') {
              j += 2;
              lastValidB = '9';
              continue;
          }
          if (static_cast<unsigned char>(aCh) > 127) {
              const auto *replacement = get_ascii_replacement(a + i);
              if (replacement) {
                  aCh = replacement->ascii;
                  i += strlen(replacement->utf8) - 1;
              }
          }
          if (static_cast<unsigned char>(bCh) > 127) {
              const auto *replacement = get_ascii_replacement(b + j);
              if (replacement) {
                  bCh = replacement->ascii;
                  j += strlen(replacement->utf8) - 1;
              }
          }
          if (::tolower(aCh) != ::tolower(bCh)) {
              return false;
          }
          lastValidA = aCh;
          lastValidB = bCh;
          ++i;
          ++j;
      }
      while (a[i] != 0 && isIgnoredChar(a[i])) {
          ++i;
      }
      while (b[j] != 0 && isIgnoredChar(b[j])) {
          ++j;
      }
      return a[i] == b[j];
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  struct PositionalAccuracy::Private {
      std::string value_{};
  };
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  PositionalAccuracy::PositionalAccuracy(const std::string &valueIn)
      : d(internal::make_unique<Private>()) {
      d->value_ = valueIn;
  }
  
  // ---------------------------------------------------------------------------
  
  //! @cond Doxygen_Suppress
  PositionalAccuracy::~PositionalAccuracy() = default;
  //! @endcond
  
  // ---------------------------------------------------------------------------
  
  /** \brief Return the value of the positional accuracy.
   */
  const std::string &PositionalAccuracy::value() PROJ_PURE_DEFN {
      return d->value_;
  }
  
  // ---------------------------------------------------------------------------
  
  /** \brief Instantiate a PositionalAccuracy.
   *
   * @param valueIn positional accuracy value.
   * @return a new PositionalAccuracy.
   */
  PositionalAccuracyNNPtr PositionalAccuracy::create(const std::string &valueIn) {
      return PositionalAccuracy::nn_make_shared<PositionalAccuracy>(valueIn);
  }
  
  } // namespace metadata
  NS_PROJ_END
  