/*
 *            Copyright 2009-2018 The VOTCA Development Team
 *                       (http://www.votca.org)
 *
 *      Licensed under the Apache License, Version 2.0 (the "License")
 *
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *              http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#include <votca/xtp/numerical_integrations.h>
#include <boost/math/constants/constants.hpp>
#include <votca/xtp/radial_euler_maclaurin_rule.h>
#include <votca/xtp/sphere_lebedev_rule.h>
#include <votca/xtp/aoshell.h>
#include <votca/tools/constants.h>

#include <votca/xtp/aomatrix.h>
#include <fstream>
#include <boost/algorithm/string.hpp>
#include <cmath>
#include <iterator>
#include <string>

namespace votca {
    namespace xtp {

    NumericalIntegration::~NumericalIntegration(){
      if (_setXC) {
        xc_func_end(&xfunc);
        if (_use_separate) {
          xc_func_end(&cfunc);
        }
      }
    }

    double NumericalIntegration::getExactExchange(const std::string& functional) {

      double exactexchange = 0.0;
      Vxc_Functionals map;
      std::vector<std::string> strs;

      boost::split(strs, functional, boost::is_any_of(" "));
      if (strs.size() > 2) {
        throw std::runtime_error("Too many functional names");
      } else if (strs.size() < 1) {
        throw std::runtime_error("Specify at least one functional");
      }

      for (unsigned i = 0; i < strs.size(); i++) {

        int func_id = map.getID(strs[i]);
        if (func_id < 0) {
          exactexchange = 0.0;
          break;
        }
        xc_func_type func;
        if (xc_func_init(&func, func_id, XC_UNPOLARIZED) != 0) {
         throw std::runtime_error((boost::format("Functional %s not found\n") %strs[i]).str());
        }
        if (exactexchange > 0 && func.cam_alpha > 0) {
          throw std::runtime_error("You have specified two functionals with exact exchange");
        }
        exactexchange += func.cam_alpha;
        xc_func_end(&func);
      }

      return exactexchange;

    }

   void NumericalIntegration::setXCfunctional(const std::string& functional) {

      Vxc_Functionals map;
      std::vector<std::string> strs;
      tools::Tokenizer tok(functional," ,\n\t");
      tok.ToVector(strs);
      xfunc_id = 0;
      _use_separate = false;
      cfunc_id = 0;
      if (strs.size() == 1) {
        xfunc_id = map.getID(strs[0]);
      } else if (strs.size() == 2) {
        xfunc_id = map.getID(strs[0]);
        cfunc_id = map.getID(strs[1]);
        _use_separate = true;
      } else {
        std::cout << "LIBXC " << strs.size() << std::endl;
        throw std::runtime_error("LIBXC. Please specify one combined or an exchange and a correlation functionals");
      }

      if (xc_func_init(&xfunc, xfunc_id, XC_UNPOLARIZED) != 0) {
        throw std::runtime_error((boost::format("Functional %s not found\n") %strs[0]).str());
      }
      if (xfunc.info->kind != 2 && !_use_separate) {
        throw std::runtime_error("Your functional misses either correlation or exchange, please specify another functional, separated by whitespace");
      }
      if (_use_separate) {
        if (xc_func_init(&cfunc, cfunc_id, XC_UNPOLARIZED) != 0) {
          throw std::runtime_error((boost::format("Functional %s not found\n") %strs[1]).str());
        }
        if ((xfunc.info->kind + cfunc.info->kind) != 1) {
          throw std::runtime_error("Your functionals are not one exchange and one correlation");
        }
      }
      _setXC = true;
      return;
    }

        void NumericalIntegration::EvaluateXC(const double rho, const double sigma, double& f_xc, double& df_drho, double& df_dsigma) {

        double exc[1];
        double vsigma[1]; // libxc
        double vrho[1]; // libxc df/drho
        switch (xfunc.info->family) {
          case XC_FAMILY_LDA:
            xc_lda_exc_vxc(&xfunc, 1, &rho, exc, vrho);
            break;
          case XC_FAMILY_GGA:
          case XC_FAMILY_HYB_GGA:
            xc_gga_exc_vxc(&xfunc, 1, &rho, &sigma, exc, vrho, vsigma);
            break;
        }
        f_xc = exc[0];
        df_drho = vrho[0];
        df_dsigma = vsigma[0];
        if (_use_separate) {
          // via libxc correlation part only
          switch (cfunc.info->family) {
            case XC_FAMILY_LDA:
              xc_lda_exc_vxc(&cfunc, 1, &rho, exc, vrho);
              break;
            case XC_FAMILY_GGA:
            case XC_FAMILY_HYB_GGA:
              xc_gga_exc_vxc(&cfunc, 1, &rho, &sigma, exc, vrho, vsigma);
              break;
          }

          f_xc += exc[0];
          df_drho += vrho[0];
          df_dsigma += vsigma[0];
        }

      return;
    }

    double NumericalIntegration::IntegratePotential(const tools::vec& rvector) {

      double result = 0.0;
      assert(_density_set && "Density not calculated");
      for (unsigned i = 0; i < _grid_boxes.size(); i++) {
        const std::vector<tools::vec>& points = _grid_boxes[i].getGridPoints();
        const std::vector<double>& weights = _grid_boxes[i].getGridWeights();
        const std::vector<double>& densities = _grid_boxes[i].getGridDensities();
        for (unsigned j = 0; j < points.size(); j++) {
          double dist = abs(points[j] - rvector);
          result -= weights[j] * densities[j] / dist;
        }
      }
      return result;
    }

  void NumericalIntegration::SortGridpointsintoBlocks(std::vector< std::vector< GridContainers::Cartesian_gridpoint > >& grid) {
      const double boxsize = 1;//1 bohr

      std::vector< std::vector< std::vector< std::vector< GridContainers::Cartesian_gridpoint* > > > > boxes;
      tools::vec min = tools::vec(std::numeric_limits<double>::max());
      tools::vec max = tools::vec(std::numeric_limits<double>::min());

      for (unsigned i = 0; i < grid.size(); i++) {
        for (unsigned j = 0; j < grid[i].size(); j++) {
          const tools::vec& pos = grid[i][j].grid_pos;
          if (pos.getX() > max.getX()) {
            max.x() = pos.getX();
          } else if (pos.getX() < min.getX()) {
            min.x() = pos.getX();
          }
          if (pos.getY() > max.getY()) {
            max.y() = pos.getY();
          } else if (pos.getY() < min.getY()) {
            min.y() = pos.getY();
          }
          if (pos.getZ() > max.getZ()) {
            max.z() = pos.getZ();
          } else if (pos.getZ() < min.getZ()) {
            min.z() = pos.getZ();
          }
        }
      }

      tools::vec molextension = (max - min);
      tools::vec numberofboxes = molextension / boxsize;
      tools::vec roundednumofbox = tools::vec(std::ceil(numberofboxes.getX()), std::ceil(numberofboxes.getY()), std::ceil(numberofboxes.getZ()));

      //creating temparray
      for (unsigned i = 0; i<unsigned(roundednumofbox.getX()); i++) {
        std::vector< std::vector< std::vector< GridContainers::Cartesian_gridpoint* > > > boxes_yz;
        for (unsigned j = 0; j<unsigned(roundednumofbox.getY()); j++) {
          std::vector< std::vector< GridContainers::Cartesian_gridpoint* > > boxes_z;
          for (unsigned k = 0; k<unsigned(roundednumofbox.getZ()); k++) {
            std::vector< GridContainers::Cartesian_gridpoint* > box;
            box.reserve(100);
            boxes_z.push_back(box);
          }
          boxes_yz.push_back(boxes_z);
        }
        boxes.push_back(boxes_yz);
      }

      for (auto & atomgrid : grid) {
        for (auto & gridpoint : atomgrid) {
          tools::vec pos = gridpoint.grid_pos - min;
          tools::vec index = pos / boxsize;
          int i_x = int(index.getX());
          int i_y = int(index.getY());
          int i_z = int(index.getZ());
          boxes[i_x][i_y][i_z].push_back(&gridpoint);
        }
      }

      for (auto& boxes_xy : boxes) {
        for (auto& boxes_z : boxes_xy) {
          for (auto& box : boxes_z) {
            if (box.size() < 1) {
              continue;
            }
            GridBox gridbox;

            for (const auto&point : box) {
              gridbox.addGridPoint(*point);
            }
            _grid_boxes.push_back(gridbox);
          }
        }
      }
      return;
    }

  void NumericalIntegration::FindSignificantShells(const AOBasis& basis) {
      for (unsigned i = 0; i < _grid_boxes.size(); ++i) {
        GridBox & box = _grid_boxes[i];
        for (const AOShell* store:basis) {
          const double decay = store->getMinDecay();
          const tools::vec& shellpos =store->getPos();
          for (const auto& point : box.getGridPoints()) {
            tools::vec dist = shellpos - point;
            double distsq = dist*dist;
            // if contribution is smaller than -ln(1e-10), add shell to list
            if ((decay * distsq) < 20.7) {
              box.addShell(store);
              break;
            }
          }
        }
      }

      std::vector< GridBox > grid_boxes_copy;
      int combined = 0;
      //use vecot of bool to indicate if a gridbox has already been merged into another
      std::vector<bool> Merged = std::vector<bool>(_grid_boxes.size(), false);
      for (unsigned i = 0; i < _grid_boxes.size(); i++) {
        if (Merged[i]) {
          continue;
        }
        GridBox box = _grid_boxes[i];
        if (box.Shellsize() < 1) {
          continue;
        }
        Merged[i] = true;
        for (unsigned j = i + 1; j < _grid_boxes.size(); j++) {
          if (GridBox::compareGridboxes(_grid_boxes[i], _grid_boxes[j])) {
            Merged[j] = true;
            box.addGridBox(_grid_boxes[j]);
            combined++;
          }
        }
        grid_boxes_copy.push_back(box);
      }
      std::vector<unsigned> sizes;
      sizes.reserve(grid_boxes_copy.size());
      for (auto& box : grid_boxes_copy) {
        sizes.push_back(box.size() * box.Matrixsize());
      }
      std::vector<unsigned> indexes = std::vector<unsigned>(sizes.size());
      std::iota(indexes.begin(), indexes.end(), 0);
      std::sort(indexes.begin(), indexes.end(), [&sizes](unsigned i1, unsigned i2) {
        return sizes[i1] > sizes[i2];
      });

      unsigned nthreads = 1;
#ifdef _OPENMP
      nthreads = omp_get_max_threads();
#endif
      std::vector<unsigned> scores = std::vector<unsigned>(nthreads, 0);
      std::vector< std::vector<unsigned> > indices;
      for (unsigned i = 0; i < nthreads; ++i) {
        std::vector<unsigned> thread_box_indices;
        indices.push_back(thread_box_indices);
      }
      for (const auto index : indexes) {
        unsigned thread = 0;
        unsigned minimum = std::numeric_limits<unsigned>::max();
        for (unsigned i = 0; i < scores.size(); ++i) {
          if (scores[i] < minimum) {
            minimum = scores[i];
            thread = i;
          }
        }
        indices[thread].push_back(index);
        scores[thread] += sizes[index];
      }
      thread_start = std::vector<unsigned>(0);
      thread_stop = std::vector<unsigned>(0);
      unsigned start = 0;
      unsigned stop = 0;
      unsigned indexoffirstgridpoint = 0;
      _grid_boxes.resize(0);
      for (const std::vector<unsigned>& thread_index : indices) {
        thread_start.push_back(start);
        stop = start + thread_index.size();
        thread_stop.push_back(stop);
        start = stop;
        for (const unsigned index : thread_index) {
          GridBox newbox = grid_boxes_copy[index];
          newbox.setIndexoffirstgridpoint(indexoffirstgridpoint);
          indexoffirstgridpoint += newbox.size();
          newbox.PrepareForIntegration();
          _grid_boxes.push_back(newbox);
        }
      }
      return;
    }

  Eigen::MatrixXd NumericalIntegration::IntegrateVXC(const Eigen::MatrixXd& density_matrix) {
      Eigen::MatrixXd Vxc = Eigen::MatrixXd::Zero(density_matrix.rows(), density_matrix.cols());
      _EXC = 0;
      unsigned nthreads = 1;
#ifdef _OPENMP
      nthreads = omp_get_max_threads();
#endif
      std::vector<Eigen::MatrixXd >vxc_thread;
      std::vector<double> Exc_thread = std::vector<double>(nthreads, 0.0);
      for (unsigned i = 0; i < nthreads; ++i) {
        Eigen::MatrixXd Vxc_thread = Eigen::MatrixXd::Zero(density_matrix.rows(), density_matrix.cols());
        vxc_thread.push_back(Vxc_thread);
      }

#pragma omp parallel for
      for (unsigned thread = 0; thread < nthreads; ++thread) {
        for (unsigned i = thread_start[thread]; i < thread_stop[thread]; ++i) {

          double EXC_box = 0.0;
          const GridBox& box = _grid_boxes[i];
          const Eigen::MatrixXd DMAT_here = box.ReadFromBigMatrix(density_matrix);
          const Eigen::MatrixXd DMAT_symm= DMAT_here+DMAT_here.transpose();
          double cutoff=1.e-40/density_matrix.rows()/density_matrix.rows();
          if (DMAT_here.cwiseAbs2().maxCoeff()<cutoff ){
            continue;
          }
          Eigen::MatrixXd Vxc_here = Eigen::MatrixXd::Zero(DMAT_here.rows(), DMAT_here.cols());
          const std::vector<tools::vec>& points = box.getGridPoints();
          const std::vector<double>& weights = box.getGridWeights();
          const std::vector<GridboxRange>& aoranges = box.getAOranges();
          const std::vector<const AOShell* >& shells = box.getShells();

          //iterate over gridpoints
          for (unsigned p = 0; p < box.size(); p++) {
            Eigen::VectorXd ao = Eigen::VectorXd::Zero(box.Matrixsize());
            Eigen::MatrixX3d ao_grad= Eigen::MatrixX3d::Zero(box.Matrixsize(),3);
            for (unsigned j = 0; j < box.Shellsize(); ++j) {
              Eigen::Block<Eigen::MatrixX3d> grad_block=ao_grad.block(aoranges[j].start,0,aoranges[j].size,3);
              Eigen::VectorBlock<Eigen::VectorXd> ao_block=ao.segment(aoranges[j].start,aoranges[j].size);
              shells[j]->EvalAOspace(ao_block,grad_block,points[p]);
            }
            const double rho =0.5* (ao.transpose()*DMAT_symm*ao).value();
            const double weight = weights[p];
            if (rho*weight < 1.e-20) continue; // skip the rest, if density is very small
            const Eigen::Vector3d rho_grad = ao.transpose()*DMAT_symm*ao_grad;
            const double sigma = (rho_grad.transpose()*rho_grad).value();
            const Eigen::VectorXd grad =ao_grad*rho_grad;
            double f_xc; // E_xc[n] = int{n(r)*eps_xc[n(r)] d3r} = int{ f_xc(r) d3r }
            double df_drho; // v_xc_rho(r) = df/drho
            double df_dsigma; // df/dsigma ( df/dgrad(rho) = df/dsigma * dsigma/dgrad(rho) = df/dsigma * 2*grad(rho))
            EvaluateXC(rho, sigma, f_xc, df_drho, df_dsigma);
            EXC_box += weight * rho * f_xc;
            auto addXC = weight * (0.5*df_drho * ao+ 2.0 * df_dsigma *grad);
            // Exchange correlation energy
            Vxc_here.noalias() += addXC* ao.transpose();
          }
          box.AddtoBigMatrix(vxc_thread[thread], Vxc_here);
          Exc_thread[thread] += EXC_box;
        }
      }
      for (unsigned i = 0; i < nthreads; ++i) {
        Vxc += vxc_thread[i];
        _EXC += Exc_thread[i];
      }
      return Vxc+Vxc.transpose();
    }

   Eigen::MatrixXd NumericalIntegration::IntegrateExternalPotential(const std::vector<double>& Potentialvalues) {

      Eigen::MatrixXd ExternalMat = Eigen::MatrixXd::Zero(_AOBasisSize, _AOBasisSize);
      unsigned nthreads = 1;
#ifdef _OPENMP
      nthreads = omp_get_max_threads();
#endif
      std::vector<Eigen::MatrixXd >vex_thread;
      std::vector<double> Exc_thread = std::vector<double>(nthreads, 0.0);
      for (unsigned i = 0; i < nthreads; ++i) {
        Eigen::MatrixXd Vex_thread = Eigen::MatrixXd::Zero(ExternalMat.rows(), ExternalMat.cols());
        vex_thread.push_back(Vex_thread);
      }

#pragma omp parallel for
      for (unsigned thread = 0; thread < nthreads; ++thread) {
        for (unsigned i = thread_start[thread]; i < thread_stop[thread]; ++i) {

          const GridBox& box = _grid_boxes[i];
          Eigen::MatrixXd Vex_here = Eigen::MatrixXd::Zero(box.Matrixsize(), box.Matrixsize());
          const std::vector<tools::vec>& points = box.getGridPoints();
          const std::vector<double>& weights = box.getGridWeights();

          //iterate over gridpoints
          for (unsigned p = 0; p < box.size(); p++) {
            Eigen::VectorXd ao = Eigen::VectorXd::Zero(box.Matrixsize());
            const std::vector<GridboxRange>& aoranges = box.getAOranges();
            const std::vector<const AOShell* > shells = box.getShells();
            for (unsigned j = 0; j < box.Shellsize(); ++j) {
              Eigen::VectorBlock<Eigen::VectorXd> ao_block=ao.segment(aoranges[j].start,aoranges[j].size);
              shells[j]->EvalAOspace(ao_block, points[p]);
            }
            Eigen::VectorXd addEX = weights[p] * Potentialvalues[box.getIndexoffirstgridpoint() + p] * ao;
            Vex_here += addEX.transpose() * ao;
          }
          box.AddtoBigMatrix(vex_thread[thread], Vex_here);
        }
      }
      for (unsigned i = 0; i < nthreads; ++i) {
        ExternalMat += vex_thread[i];
      }
      ExternalMat += ExternalMat.transpose();
      return ExternalMat;
    }

  double NumericalIntegration::IntegrateDensity(const Eigen::MatrixXd& density_matrix) {
      double N = 0;
      unsigned nthreads = 1;
#ifdef _OPENMP
      nthreads = omp_get_max_threads();
#endif
      std::vector<double> N_thread = std::vector<double>(nthreads, 0.0);

#pragma omp parallel for
      for (unsigned thread = 0; thread < nthreads; ++thread) {
        for (unsigned i = thread_start[thread]; i < thread_stop[thread]; ++i) {

          double N_box = 0.0;
          GridBox& box = _grid_boxes[i];
          const Eigen::MatrixXd DMAT_here = box.ReadFromBigMatrix(density_matrix);
          const std::vector<tools::vec>& points = box.getGridPoints();
          const std::vector<double>& weights = box.getGridWeights();
          box.prepareDensity();
          //iterate over gridpoints
          for (unsigned p = 0; p < box.size(); p++) {
            Eigen::VectorXd ao = Eigen::VectorXd::Zero(box.Matrixsize());
            const std::vector<GridboxRange>& aoranges = box.getAOranges();
            const std::vector<const AOShell* > shells = box.getShells();
            for (unsigned j = 0; j < box.Shellsize(); ++j) {
              Eigen::VectorBlock<Eigen::VectorXd> ao_block=ao.segment(aoranges[j].start,aoranges[j].size);
              shells[j]->EvalAOspace(ao_block, points[p]);
            }
            double rho =(ao.transpose()*DMAT_here*ao)(0, 0);
            box.addDensity(rho);
            N_box += rho * weights[p];
          }
          N_thread[thread] += N_box;
        }
      }
      for (unsigned i = 0; i < nthreads; ++i) {
        N += N_thread[i];
      }
      _density_set = true;
      return N;
    }

      Gyrationtensor NumericalIntegration::IntegrateGyrationTensor(const Eigen::MatrixXd& density_matrix) {
      double N = 0;
      tools::vec centroid = tools::vec(0.0);
      tools::matrix gyration = tools::matrix(0.0);
      unsigned nthreads = 1;
#ifdef _OPENMP
      nthreads = omp_get_max_threads();
#endif
      std::vector<double> N_thread = std::vector<double>(nthreads, 0.0);
      // centroid
      std::vector<tools::vec> centroid_thread;
      std::vector<tools::matrix> gyration_thread;
      for (unsigned thread = 0; thread < nthreads; ++thread) {
        tools::vec tempvec = tools::vec(0.0);
        centroid_thread.push_back(tempvec);
        tools::matrix tempmatrix = tools::matrix(0.0);
        gyration_thread.push_back(tempmatrix);
      }

#pragma omp parallel for
      for (unsigned thread = 0; thread < nthreads; ++thread) {
        for (unsigned i = thread_start[thread]; i < thread_stop[thread]; ++i) {
          double N_box = 0.0;
          tools::vec centroid_box = tools::vec(0.0);
          tools::matrix gyration_box = tools::matrix(0.0);
          GridBox& box = _grid_boxes[i];
          const Eigen::MatrixXd DMAT_here = box.ReadFromBigMatrix(density_matrix);
          const std::vector<tools::vec>& points = box.getGridPoints();
          const std::vector<double>& weights = box.getGridWeights();
          box.prepareDensity();
          //iterate over gridpoints
          for (unsigned p = 0; p < box.size(); p++) {
            Eigen::VectorXd ao = Eigen::VectorXd::Zero(box.Matrixsize());
            const std::vector<GridboxRange>& aoranges = box.getAOranges();
            const std::vector<const AOShell* > shells = box.getShells();
            for (unsigned j = 0; j < box.Shellsize(); ++j) {
              Eigen::VectorBlock<Eigen::VectorXd> ao_block=ao.segment(aoranges[j].start,aoranges[j].size);
              shells[j]->EvalAOspace(ao_block, points[p]);
            }
            double rho =(ao.transpose()*DMAT_here*ao)(0, 0);
            box.addDensity(rho);
            N_box += rho * weights[p];
            centroid_box+=rho * weights[p] * points[p];
            gyration_box += rho * weights[p] * (points[p]|points[p]);
          }
          N_thread[thread] += N_box;
          centroid_thread[thread] += centroid_box;
          gyration_thread[thread] += gyration_box;
        }
      }
      for (unsigned i = 0; i < nthreads; ++i) {
        N += N_thread[i];
        centroid += centroid_thread[i];
        gyration += gyration_thread[i];
      }
      _density_set = true;
      // Normalize
      centroid = centroid / N;
      gyration = gyration / N;
      gyration=gyration-(centroid|centroid);
      Gyrationtensor gyro;
      gyro.mass=N;
      gyro.centroid=centroid;
      gyro.gyration=gyration;

      return gyro;
    }

std::vector<const tools::vec *> NumericalIntegration::getGridpoints() const{
    std::vector<const tools::vec *> gridpoints;
    for (unsigned i = 0; i < _grid_boxes.size(); i++) {
      const std::vector<tools::vec>& points = _grid_boxes[i].getGridPoints();
      for (unsigned j = 0; j < points.size(); j++) {
        gridpoints.push_back(&points[j]);
      }
    }
    return gridpoints;
  }

Eigen::MatrixXd NumericalIntegration::IntegratePotential(const AOBasis& externalbasis){
  Eigen::MatrixXd Potential=Eigen::MatrixXd::Zero(externalbasis.AOBasisSize(),externalbasis.AOBasisSize());

  assert(_density_set && "Density not calculated");
  for (unsigned i = 0; i < _grid_boxes.size(); i++) {
    const std::vector<tools::vec>& points = _grid_boxes[i].getGridPoints();
    const std::vector<double>& weights = _grid_boxes[i].getGridWeights();
    const std::vector<double>& densities = _grid_boxes[i].getGridDensities();
    for (unsigned j = 0; j < points.size(); j++) {
      double weighteddensity=weights[j]*densities[j];
      if (weighteddensity<1e-12){
        continue;
      }
      AOESP esp;
      esp.setPosition(points[j]);
      esp.Fill(externalbasis);
      Potential+=weighteddensity*esp.Matrix();
    }
  }
  return Potential;
}

Eigen::MatrixXd NumericalIntegration::CalcInverseAtomDist(std::vector<QMAtom*>& atoms){
  Eigen::MatrixXd result=Eigen::MatrixXd::Zero(atoms.size(),atoms.size());
#pragma omp parallel for
  for (unsigned i=0;i<atoms.size();++i) {
    QMAtom* atom_a=atoms[i];
    const tools::vec& pos_a = atom_a->getPos();
   for (unsigned j=0;j<i;++j) {
      QMAtom* atom_b=atoms[j];
      const tools::vec& pos_b = atom_b->getPos();
      result(j,i)=1/tools::abs(pos_a-pos_b);
    }
  }
  return result+result.transpose();
}

int NumericalIntegration::UpdateOrder(LebedevGrid& sphericalgridofElement, int maxorder, std::vector<double>& PruningIntervals, double r){
  int order;
  int maxindex=sphericalgridofElement.getIndexFromOrder(maxorder);
  if (maxindex == 1) {
    // smallest possible grid anyway, nothing to do
    order = maxorder;
  } else if (maxindex == 2) {
    // only three intervals
    if (r < PruningIntervals[0]) {
      order = sphericalgridofElement.getOrderFromIndex(1); //1;
    } else if ((r >= PruningIntervals[0]) && (r < PruningIntervals[3])) {
      order = sphericalgridofElement.getOrderFromIndex(2);
    } else {
      order = sphericalgridofElement.getOrderFromIndex(1);
    } // maxorder == 2
  } else {
    // five intervals
    if (r < PruningIntervals[0]) {
      order = sphericalgridofElement.getOrderFromIndex(2);
    } else if ((r >= PruningIntervals[0]) && (r < PruningIntervals[1])) {
      order = sphericalgridofElement.getOrderFromIndex(4);
    } else if ((r >= PruningIntervals[1]) && (r < PruningIntervals[2])) {
      order = sphericalgridofElement.getOrderFromIndex(std::max(maxindex - 1, 4));
    } else if ((r >= PruningIntervals[2]) && (r < PruningIntervals[3])) {
      order = maxorder;
    } else {
      order = sphericalgridofElement.getOrderFromIndex(std::max(maxindex - 1, 1));
    }
  }
  return order;
    }

    GridContainers::Cartesian_gridpoint NumericalIntegration::CreateCartesianGridpoint(const tools::vec& atomA_pos,
            GridContainers::radial_grid& radial_grid, GridContainers::spherical_grid& spherical_grid,
            unsigned i_rad, unsigned i_sph) {
      GridContainers::Cartesian_gridpoint gridpoint;
      double p = spherical_grid.phi[i_sph];
      double t = spherical_grid.theta[i_sph];
      const tools::vec s = tools::vec(sin(p) * cos(t), sin(p) * sin(t), cos(p));
      double r = radial_grid.radius[i_rad];
      gridpoint.grid_pos = atomA_pos + r*s;
      gridpoint.grid_weight = radial_grid.weight[i_rad] * spherical_grid.weight[i_sph];
      return gridpoint;
    }

    Eigen::MatrixXd NumericalIntegration::CalcDistanceAtomsGridpoints(std::vector<QMAtom*>& atoms, std::vector<GridContainers::Cartesian_gridpoint>& atomgrid){
      Eigen::MatrixXd result=Eigen::MatrixXd::Zero(atoms.size(),atomgrid.size());
     #pragma omp parallel for
      for (unsigned i=0;i<atoms.size();++i) {
        QMAtom* atom=atoms[i];
        const tools::vec & atom_pos =atom->getPos();
        for (unsigned j=0;j<atomgrid.size();++j) {
          const auto& gridpoint=atomgrid[j];
          result(i,j)=tools::abs(atom_pos-gridpoint.grid_pos);
        }
      }
      return result;
    }

    void NumericalIntegration::SSWpartitionAtom(std::vector<QMAtom*>& atoms, std::vector<GridContainers::Cartesian_gridpoint>& atomgrid
                                                , unsigned i_atom, const Eigen::MatrixXd& Rij){
      Eigen::MatrixXd AtomGridDist=CalcDistanceAtomsGridpoints(atoms, atomgrid);

#pragma omp parallel for schedule(guided)
      for (unsigned i_grid = 0; i_grid < atomgrid.size(); i_grid++) {
        Eigen::VectorXd p = SSWpartition(i_grid, AtomGridDist,Rij);
        // check weight sum
        double wsum = p.sum();
        if (wsum != 0.0) {
          // update the weight of this grid point
          atomgrid[i_grid].grid_weight *= p[i_atom] / wsum;
        } else {
          std::cerr << "\nSum of partition weights of grid point " << i_grid << " of atom " << i_atom << " is zero! ";
          throw std::runtime_error("\nThis should never happen!");
        }
      } // partition weight for each gridpoint
    }

void NumericalIntegration::GridSetup(const std::string& type, std::vector<QMAtom*> atoms,const AOBasis& basis) {
      _AOBasisSize=basis.AOBasisSize();
      GridContainers initialgrids;
      // get radial grid per element
      EulerMaclaurinGrid radialgridofElement;
      initialgrids.radial_grids=radialgridofElement.CalculateAtomicRadialGrids(basis, atoms, type); // this checks out 1:1 with NWChem results! AWESOME
      LebedevGrid sphericalgridofElement;
      initialgrids.spherical_grids=sphericalgridofElement.CalculateSphericalGrids(atoms,type);

      // for the partitioning, we need all inter-center distances later, stored in matrix
      Eigen::MatrixXd Rij=CalcInverseAtomDist(atoms);
      _totalgridsize = 0;
      std::vector< std::vector< GridContainers::Cartesian_gridpoint > > grid;

      for (unsigned i_atom=0;i_atom<atoms.size();++i_atom) {
        QMAtom* atom=atoms[i_atom];

        const tools::vec & atomA_pos =atom->getPos();
        const std::string & name = atom->getType();
        GridContainers::radial_grid radial_grid = initialgrids.radial_grids.at(name);
        GridContainers::spherical_grid spherical_grid = initialgrids.spherical_grids.at(name);

        // maximum order (= number of points) in spherical integration grid
        int maxorder = sphericalgridofElement.Type2MaxOrder(name, type);
        // for pruning of integration grid, get interval boundaries for this element
        std::vector<double> PruningIntervals = radialgridofElement.CalculatePruningIntervals(name);
        int current_order = 0;
        // for each radial value
        std::vector< GridContainers::Cartesian_gridpoint > atomgrid;
        for (unsigned i_rad = 0; i_rad < radial_grid.radius.size(); i_rad++) {
          double r = radial_grid.radius[i_rad];

          // which Lebedev order for this point?
          int order=UpdateOrder(sphericalgridofElement, maxorder, PruningIntervals, r);
          // get new spherical grid, if order changed
          if (order != current_order) {
            spherical_grid=sphericalgridofElement.CalculateUnitSphereGrid(order);
            current_order = order;
          }

          for (unsigned i_sph = 0; i_sph < spherical_grid.phi.size(); i_sph++) {
            GridContainers::Cartesian_gridpoint gridpoint=CreateCartesianGridpoint(atomA_pos, radial_grid, spherical_grid, i_rad,i_sph);
            atomgrid.push_back(gridpoint);
          } // spherical gridpoints
        } // radial gridpoint

        SSWpartitionAtom(atoms, atomgrid, i_atom,Rij);

        // now remove points from the grid with negligible weights
        for (std::vector<GridContainers::Cartesian_gridpoint >::iterator git = atomgrid.begin(); git != atomgrid.end();) {
          if (git->grid_weight < 1e-13) {
            git = atomgrid.erase(git);
          } else {
            ++git;
          }
        }
        _totalgridsize += atomgrid.size();
        grid.push_back(atomgrid);
      } // atoms
      SortGridpointsintoBlocks(grid);
      FindSignificantShells(basis);
      return;
    }

    Eigen::VectorXd NumericalIntegration::SSWpartition(int igrid, const Eigen::MatrixXd & rq,const Eigen::MatrixXd& Rij) {
      const double ass = 0.725;
      // initialize partition vector to 1.0
      Eigen::VectorXd p=Eigen::VectorXd::Ones(rq.rows());
      const double tol_scr = 1e-10;
      const double leps = 1e-6;
      // go through centers
      for (int i = 1; i < rq.rows(); i++) {
        double rag = rq(i,igrid);
        // through all other centers (one-directional)
        for (int j = 0; j < i; j++) {
          if ((std::abs(p[i]) > tol_scr) || (std::abs(p[j]) > tol_scr)) {
            double mu = (rag - rq(j,igrid)) * Rij(j,i);
            if (mu > ass) {
              p[i] = 0.0;
            } else if (mu < -ass) {
              p[j] = 0.0;
            } else {
              double sk;
              if (std::abs(mu) < leps) {
                sk = -1.88603178008 * mu + 0.5;
              } else {
                sk =erf1c(mu);
              }
              if (mu > 0.0) sk = 1.0 - sk;
              p[j] = p[j] * sk;
              p[i] = p[i] * (1.0 - sk);
            }
          }
        }
      }
      return p;
    }

    double NumericalIntegration::erf1c(double x){
        const static double alpha_erf1=1.0/0.30;
        return 0.5*std::erfc(std::abs(x/(1.0-x*x))*alpha_erf1);
    }

    }
}


// Boost.Geometry - gis-projections (based on PROJ4)

// Copyright (c) 2008-2015 Barend Gehrels, Amsterdam, the Netherlands.

// This file was modified by Oracle on 2017, 2018.
// Modifications copyright (c) 2017-2018, Oracle and/or its affiliates.
// Contributed and/or modified by Adam Wulkiewicz, on behalf of Oracle.

// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

// This file is converted from PROJ4, http://trac.osgeo.org/proj
// PROJ4 is originally written by Gerald Evenden (then of the USGS)
// PROJ4 is maintained by Frank Warmerdam
// PROJ4 is converted to Boost.Geometry by Barend Gehrels

// Last updated version of proj: 5.0.0

// Original copyright notice:

// This code was entirely written by Nathan Wagner
// and is in the public domain.

// 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 BOOST_GEOMETRY_PROJECTIONS_ISEA_HPP
#define BOOST_GEOMETRY_PROJECTIONS_ISEA_HPP

#include <sstream>

#include <boost/core/ignore_unused.hpp>
#include <boost/geometry/util/math.hpp>

#include <boost/geometry/srs/projections/impl/base_static.hpp>
#include <boost/geometry/srs/projections/impl/base_dynamic.hpp>
#include <boost/geometry/srs/projections/impl/projects.hpp>
#include <boost/geometry/srs/projections/impl/factory_entry.hpp>

namespace boost { namespace geometry
{

namespace projections
{
    #ifndef DOXYGEN_NO_DETAIL
    namespace detail { namespace isea
    {
            static const double epsilon = std::numeric_limits<double>::epsilon();

            /* sqrt(5)/M_PI */
            static const double isea_scale = 0.8301572857837594396028083;
            /* 26.565051177 degrees */
            static const double v_lat = 0.46364760899944494524;
            /* 52.62263186 */
            static const double e_rad = 0.91843818702186776133;
            /* 10.81231696 */
            static const double f_rad = 0.18871053072122403508;
            /* R tan(g) sin(60) */
            static const double table_g = 0.6615845383;
            /* H = 0.25 R tan g = */
            static const double table_h = 0.1909830056;
            //static const double RPRIME = 0.91038328153090290025;
            static const double isea_std_lat = 1.01722196792335072101;
            static const double isea_std_lon = .19634954084936207740;

            template <typename T>
            inline T deg30_rad() { return T(30) * geometry::math::d2r<T>(); }
            template <typename T>
            inline T deg120_rad() { return T(120) * geometry::math::d2r<T>(); }
            template <typename T>
            inline T deg72_rad() { return T(72) * geometry::math::d2r<T>(); }
            template <typename T>
            inline T deg90_rad() { return geometry::math::half_pi<T>(); }
            template <typename T>
            inline T deg144_rad() { return T(144) * geometry::math::d2r<T>(); }
            template <typename T>
            inline T deg36_rad() { return T(36) * geometry::math::d2r<T>(); }
            template <typename T>
            inline T deg108_rad() { return T(108) * geometry::math::d2r<T>(); }
            template <typename T>
            inline T deg180_rad() { return geometry::math::pi<T>(); }

            inline bool downtri(int tri) { return (((tri - 1) / 5) % 2 == 1); }

            /*
             * Proj 4 provides its own entry points into
             * the code, so none of the library functions
             * need to be global
             */

            struct hex {
                    int iso;
                    int x, y, z;
            };

            /* y *must* be positive down as the xy /iso conversion assumes this */
            inline
            int hex_xy(struct hex *h) {
                if (!h->iso) return 1;
                if (h->x >= 0) {
                    h->y = -h->y - (h->x+1)/2;
                } else {
                    /* need to round toward -inf, not toward zero, so x-1 */
                    h->y = -h->y - h->x/2;
                }
                h->iso = 0;

                return 1;
            }

            inline
            int hex_iso(struct hex *h) {
                if (h->iso) return 1;

                if (h->x >= 0) {
                    h->y = (-h->y - (h->x+1)/2);
                } else {
                    /* need to round toward -inf, not toward zero, so x-1 */
                    h->y = (-h->y - (h->x)/2);
                }

                h->z = -h->x - h->y;
                h->iso = 1;
                return 1;
            }

            template <typename T>
            inline
            int hexbin2(T const& width, T x, T y, int *i, int *j)
            {
                T z, rx, ry, rz;
                T abs_dx, abs_dy, abs_dz;
                int ix, iy, iz, s;
                struct hex h;

                static const T cos_deg30 = cos(deg30_rad<T>());

                x = x / cos_deg30; /* rotated X coord */
                y = y - x / 2.0; /* adjustment for rotated X */

                /* adjust for actual hexwidth */
                x /= width;
                y /= width;

                z = -x - y;

                rx = floor(x + 0.5);
                ix = (int)rx;
                ry = floor(y + 0.5);
                iy = (int)ry;
                rz = floor(z + 0.5);
                iz = (int)rz;

                s = ix + iy + iz;

                if (s) {
                    abs_dx = fabs(rx - x);
                    abs_dy = fabs(ry - y);
                    abs_dz = fabs(rz - z);

                    if (abs_dx >= abs_dy && abs_dx >= abs_dz) {
                        ix -= s;
                    } else if (abs_dy >= abs_dx && abs_dy >= abs_dz) {
                        iy -= s;
                    } else {
                        iz -= s;
                    }
                }
                h.x = ix;
                h.y = iy;
                h.z = iz;
                h.iso = 1;

                hex_xy(&h);
                *i = h.x;
                *j = h.y;
                return ix * 100 + iy;
            }

            //enum isea_poly { isea_none = 0, isea_icosahedron = 20 };
            //enum isea_topology { isea_hexagon=6, isea_triangle=3, isea_diamond=4 };
            enum isea_address_form {
                isea_addr_geo, isea_addr_q2di, isea_addr_seqnum,
                isea_addr_interleave, isea_addr_plane, isea_addr_q2dd,
                isea_addr_projtri, isea_addr_vertex2dd, isea_addr_hex
            };

            template <typename T>
            struct isea_dgg {
                //isea_poly         polyhedron; /* ignored, icosahedron */
                T                 o_lat, o_lon, o_az; /* orientation, radians */
                int               pole; /* true if standard snyder */
                //isea_topology     topology; /* ignored, hexagon */
                int               aperture; /* valid values depend on partitioning method */
                int               resolution;
                T                 radius; /* radius of the earth in meters, ignored 1.0 */
                isea_address_form output; /* an isea_address_form */
                int               triangle; /* triangle of last transformed point */
                int               quad; /* quad of last transformed point */
                unsigned long     serial;
            };

            template <typename T>
            struct isea_pt {
                T x, y;
            };

            template <typename T>
            struct isea_geo {
                T lon, lat;
            };

            template <typename T>
            struct isea_address {
                int    type; /* enum isea_address_form */
                int    number;
                T      x,y; /* or i,j or lon,lat depending on type */
            };

            /* ENDINC */

            enum snyder_polyhedron {
                snyder_poly_hexagon = 0, snyder_poly_pentagon = 1,
                snyder_poly_tetrahedron = 2, snyder_poly_cube = 3,
                snyder_poly_octahedron = 4, snyder_poly_dodecahedron = 5,
                snyder_poly_icosahedron = 6
            };

            template <typename T>
            struct snyder_constants {
                T          g, G, theta, ea_w, ea_a, ea_b, g_w, g_a, g_b;
            };

            template <typename T>
            inline const snyder_constants<T> * constants()
            {
                /* TODO put these in radians to avoid a later conversion */
                static snyder_constants<T> result[] = {
                    {23.80018260, 62.15458023, 60.0, 3.75, 1.033, 0.968, 5.09, 1.195, 1.0},
                    {20.07675127, 55.69063953, 54.0, 2.65, 1.030, 0.983, 3.59, 1.141, 1.027},
                    {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                    {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                    {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                    {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
                    {37.37736814, 36.0, 30.0, 17.27, 1.163, 0.860, 13.14, 1.584, 1.0}
                };
                return result;
            }

            template <typename T>
            inline const isea_geo<T> * vertex()
            {
                static isea_geo<T> result[] = {
                    { 0.0,              deg90_rad<T>()},
                    { deg180_rad<T>(),  v_lat},
                    {-deg108_rad<T>(),  v_lat},
                    {-deg36_rad<T>(),   v_lat},
                    { deg36_rad<T>(),   v_lat},
                    { deg108_rad<T>(),  v_lat},
                    {-deg144_rad<T>(), -v_lat},
                    {-deg72_rad<T>(),  -v_lat},
                    { 0.0,             -v_lat},
                    { deg72_rad<T>(),  -v_lat},
                    { deg144_rad<T>(), -v_lat},
                    { 0.0,             -deg90_rad<T>()}
                };
                return result;
            }

            /* TODO make an isea_pt array of the vertices as well */

            static int      tri_v1[] = {0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 2, 3, 4, 5, 1, 11, 11, 11, 11, 11};

            /* triangle Centers */
            template <typename T>
            inline const isea_geo<T> * icostriangles()
            {
                static isea_geo<T> result[] = {
                    { 0.0,              0.0},
                    {-deg144_rad<T>(),  e_rad},
                    {-deg72_rad<T>(),   e_rad},
                    { 0.0,              e_rad},
                    { deg72_rad<T>(),   e_rad},
                    { deg144_rad<T>(),  e_rad},
                    {-deg144_rad<T>(),  f_rad},
                    {-deg72_rad<T>(),   f_rad},
                    { 0.0,              f_rad},
                    { deg72_rad<T>(),   f_rad},
                    { deg144_rad<T>(),  f_rad},
                    {-deg108_rad<T>(), -f_rad},
                    {-deg36_rad<T>(),  -f_rad},
                    { deg36_rad<T>(),  -f_rad},
                    { deg108_rad<T>(), -f_rad},
                    { deg180_rad<T>(), -f_rad},
                    {-deg108_rad<T>(), -e_rad},
                    {-deg36_rad<T>(),  -e_rad},
                    { deg36_rad<T>(),  -e_rad},
                    { deg108_rad<T>(), -e_rad},
                    { deg180_rad<T>(), -e_rad},
                };
                return result;
            }

            template <typename T>
            inline T az_adjustment(int triangle)
            {
                T          adj;

                isea_geo<T> v;
                isea_geo<T> c;

                v = vertex<T>()[tri_v1[triangle]];
                c = icostriangles<T>()[triangle];

                /* TODO looks like the adjustment is always either 0 or 180 */
                /* at least if you pick your vertex carefully */
                adj = atan2(cos(v.lat) * sin(v.lon - c.lon),
                        cos(c.lat) * sin(v.lat)
                        - sin(c.lat) * cos(v.lat) * cos(v.lon - c.lon));
                return adj;
            }

            template <typename T>
            inline isea_pt<T> isea_triangle_xy(int triangle)
            {
                isea_pt<T>  c;
                T Rprime = 0.91038328153090290025;

                triangle = (triangle - 1) % 20;

                c.x = table_g * ((triangle % 5) - 2) * 2.0;
                if (triangle > 9) {
                    c.x += table_g;
                }
                switch (triangle / 5) {
                case 0:
                    c.y = 5.0 * table_h;
                    break;
                case 1:
                    c.y = table_h;
                    break;
                case 2:
                    c.y = -table_h;
                    break;
                case 3:
                    c.y = -5.0 * table_h;
                    break;
                default:
                    /* should be impossible */
                    BOOST_THROW_EXCEPTION( projection_exception() );
                };
                c.x *= Rprime;
                c.y *= Rprime;

                return c;
            }

            /* snyder eq 14 */
            template <typename T>
            inline T sph_azimuth(T const& f_lon, T const& f_lat, T const& t_lon, T const& t_lat)
            {
                T          az;

                az = atan2(cos(t_lat) * sin(t_lon - f_lon),
                       cos(f_lat) * sin(t_lat)
                       - sin(f_lat) * cos(t_lat) * cos(t_lon - f_lon)
                    );
                return az;
            }

            /* coord needs to be in radians */
            template <typename T>
            inline int isea_snyder_forward(isea_geo<T> * ll, isea_pt<T> * out)
            {
                static T const two_pi = detail::two_pi<T>();
                static T const d2r = geometry::math::d2r<T>();

                int             i;

                /*
                 * spherical distance from center of polygon face to any of its
                 * vertexes on the globe
                 */
                T          g;

                /*
                 * spherical angle between radius vector to center and adjacent edge
                 * of spherical polygon on the globe
                 */
                T          G;

                /*
                 * plane angle between radius vector to center and adjacent edge of
                 * plane polygon
                 */
                T          theta;

                /* additional variables from snyder */
                T          q, Rprime, H, Ag, Azprime, Az, dprime, f, rho,
                                x, y;

                /* variables used to store intermediate results */
                T          cot_theta, tan_g, az_offset;

                /* how many multiples of 60 degrees we adjust the azimuth */
                int             Az_adjust_multiples;

                snyder_constants<T> c;

                /*
                 * TODO by locality of reference, start by trying the same triangle
                 * as last time
                 */

                /* TODO put these constants in as radians to begin with */
                c = constants<T>()[snyder_poly_icosahedron];
                theta = c.theta * d2r;
                g = c.g * d2r;
                G = c.G * d2r;

                for (i = 1; i <= 20; i++) {
                    T          z;
                    isea_geo<T> center;

                    center = icostriangles<T>()[i];

                    /* step 1 */
                    z = acos(sin(center.lat) * sin(ll->lat)
                         + cos(center.lat) * cos(ll->lat) * cos(ll->lon - center.lon));

                    /* not on this triangle */
                    if (z > g + 0.000005) { /* TODO DBL_EPSILON */
                        continue;
                    }

                    Az = sph_azimuth(center.lon, center.lat, ll->lon, ll->lat);

                    /* step 2 */

                    /* This calculates "some" vertex coordinate */
                    az_offset = az_adjustment<T>(i);

                    Az -= az_offset;

                    /* TODO I don't know why we do this.  It's not in snyder */
                    /* maybe because we should have picked a better vertex */
                    if (Az < 0.0) {
                        Az += two_pi;
                    }
                    /*
                     * adjust Az for the point to fall within the range of 0 to
                     * 2(90 - theta) or 60 degrees for the hexagon, by
                     * and therefore 120 degrees for the triangle
                     * of the icosahedron
                     * subtracting or adding multiples of 60 degrees to Az and
                     * recording the amount of adjustment
                     */

                    Az_adjust_multiples = 0;
                    while (Az < 0.0) {
                        Az += deg120_rad<T>();
                        Az_adjust_multiples--;
                    }
                    while (Az > deg120_rad<T>() + epsilon) {
                        Az -= deg120_rad<T>();
                        Az_adjust_multiples++;
                    }

                    /* step 3 */
                    cot_theta = 1.0 / tan(theta);
                    tan_g = tan(g);    /* TODO this is a constant */

                    /* Calculate q from eq 9. */
                    /* TODO cot_theta is cot(30) */
                    q = atan2(tan_g, cos(Az) + sin(Az) * cot_theta);

                    /* not in this triangle */
                    if (z > q + 0.000005) {
                        continue;
                    }
                    /* step 4 */

                    /* Apply equations 5-8 and 10-12 in order */

                    /* eq 5 */
                    /* Rprime = 0.9449322893 * R; */
                    /* R' in the paper is for the truncated */
                    Rprime = 0.91038328153090290025;

                    /* eq 6 */
                    H = acos(sin(Az) * sin(G) * cos(g) - cos(Az) * cos(G));

                    /* eq 7 */
                    /* Ag = (Az + G + H - deg180_rad) * M_PI * R * R / deg180_rad; */
                    Ag = Az + G + H - deg180_rad<T>();

                    /* eq 8 */
                    Azprime = atan2(2.0 * Ag, Rprime * Rprime * tan_g * tan_g - 2.0 * Ag * cot_theta);

                    /* eq 10 */
                    /* cot(theta) = 1.73205080756887729355 */
                    dprime = Rprime * tan_g / (cos(Azprime) + sin(Azprime) * cot_theta);

                    /* eq 11 */
                    f = dprime / (2.0 * Rprime * sin(q / 2.0));

                    /* eq 12 */
                    rho = 2.0 * Rprime * f * sin(z / 2.0);

                    /*
                     * add back the same 60 degree multiple adjustment from step
                     * 2 to Azprime
                     */

                    Azprime += deg120_rad<T>() * Az_adjust_multiples;

                    /* calculate rectangular coordinates */

                    x = rho * sin(Azprime);
                    y = rho * cos(Azprime);

                    /*
                     * TODO
                     * translate coordinates to the origin for the particular
                     * hexagon on the flattened polyhedral map plot
                     */

                    out->x = x;
                    out->y = y;

                    return i;
                }

                /*
                 * should be impossible, this implies that the coordinate is not on
                 * any triangle
                 */

                //fprintf(stderr, "impossible transform: %f %f is not on any triangle\n",
                //    ll->lon * geometry::math::r2d<double>(), ll->lat * geometry::math::r2d<double>());
                std::stringstream ss;
                ss << "impossible transform: " << ll->lon * geometry::math::r2d<T>()
                   << " " << ll->lat * geometry::math::r2d<T>() << " is not on any triangle.";

                BOOST_THROW_EXCEPTION( projection_exception(ss.str()) );

                /* not reached */
                return 0;        /* supresses a warning */
            }

            /*
             * return the new coordinates of any point in orginal coordinate system.
             * Define a point (newNPold) in orginal coordinate system as the North Pole in
             * new coordinate system, and the great circle connect the original and new
             * North Pole as the lon0 longitude in new coordinate system, given any point
             * in orginal coordinate system, this function return the new coordinates.
             */

            /* formula from Snyder, Map Projections: A working manual, p31 */
            /*
             * old north pole at np in new coordinates
             * could be simplified a bit with fewer intermediates
             *
             * TODO take a result pointer
             */
            template <typename T>
            inline isea_geo<T> snyder_ctran(isea_geo<T> * np, isea_geo<T> * pt)
            {
                static T const pi = detail::pi<T>();
                static T const two_pi = detail::two_pi<T>();

                isea_geo<T> npt;
                T           alpha, phi, lambda, lambda0, beta, lambdap, phip;
                T           sin_phip;
                T           lp_b;    /* lambda prime minus beta */
                T           cos_p, sin_a;

                phi = pt->lat;
                lambda = pt->lon;
                alpha = np->lat;
                beta = np->lon;
                lambda0 = beta;

                cos_p = cos(phi);
                sin_a = sin(alpha);

                /* mpawm 5-7 */
                sin_phip = sin_a * sin(phi) - cos(alpha) * cos_p * cos(lambda - lambda0);

                /* mpawm 5-8b */

                /* use the two argument form so we end up in the right quadrant */
                lp_b = atan2(cos_p * sin(lambda - lambda0),
                   (sin_a * cos_p * cos(lambda - lambda0) + cos(alpha) * sin(phi)));

                lambdap = lp_b + beta;

                /* normalize longitude */
                /* TODO can we just do a modulus ? */
                lambdap = fmod(lambdap, two_pi);
                while (lambdap > pi)
                    lambdap -= two_pi;
                while (lambdap < -pi)
                    lambdap += two_pi;

                phip = asin(sin_phip);

                npt.lat = phip;
                npt.lon = lambdap;

                return npt;
            }

            template <typename T>
            inline isea_geo<T> isea_ctran(isea_geo<T> * np, isea_geo<T> * pt, T const& lon0)
            {
                static T const pi = detail::pi<T>();
                static T const two_pi = detail::two_pi<T>();

                isea_geo<T> npt;

                np->lon += pi;
                npt = snyder_ctran(np, pt);
                np->lon -= pi;

                npt.lon -= (pi - lon0 + np->lon);

                /*
                 * snyder is down tri 3, isea is along side of tri1 from vertex 0 to
                 * vertex 1 these are 180 degrees apart
                 */
                npt.lon += pi;
                /* normalize longitude */
                npt.lon = fmod(npt.lon, two_pi);
                while (npt.lon > pi)
                    npt.lon -= two_pi;
                while (npt.lon < -pi)
                    npt.lon += two_pi;

                return npt;
            }

            /* in radians */

            /* fuller's at 5.2454 west, 2.3009 N, adjacent at 7.46658 deg */

            template <typename T>
            inline int isea_grid_init(isea_dgg<T> * g)
            {
                if (!g)
                    return 0;

                //g->polyhedron = isea_icosahedron;
                g->o_lat = isea_std_lat;
                g->o_lon = isea_std_lon;
                g->o_az = 0.0;
                g->aperture = 4;
                g->resolution = 6;
                g->radius = 1.0;
                //g->topology = isea_hexagon;

                return 1;
            }

            template <typename T>
            inline int isea_orient_isea(isea_dgg<T> * g)
            {
                if (!g)
                    return 0;
                g->o_lat = isea_std_lat;
                g->o_lon = isea_std_lon;
                g->o_az = 0.0;
                return 1;
            }

            template <typename T>
            inline int isea_orient_pole(isea_dgg<T> * g)
            {
                static T const half_pi = detail::half_pi<T>();

                if (!g)
                    return 0;
                g->o_lat = half_pi;
                g->o_lon = 0.0;
                g->o_az = 0;
                return 1;
            }

            template <typename T>
            inline int isea_transform(isea_dgg<T> * g, isea_geo<T> * in,
                                      isea_pt<T> * out)
            {
                isea_geo<T> i, pole;
                int         tri;

                pole.lat = g->o_lat;
                pole.lon = g->o_lon;

                i = isea_ctran(&pole, in, g->o_az);

                tri = isea_snyder_forward(&i, out);
                out->x *= g->radius;
                out->y *= g->radius;
                g->triangle = tri;

                return tri;
            }

            template <typename T>
            inline void isea_rotate(isea_pt<T> * pt, T const& degrees)
            {
                static T const d2r = geometry::math::d2r<T>();
                static T const two_pi = detail::two_pi<T>();

                T          rad;

                T          x, y;

                rad = -degrees * d2r;
                while (rad >= two_pi) rad -= two_pi;
                while (rad <= -two_pi) rad += two_pi;

                x = pt->x * cos(rad) + pt->y * sin(rad);
                y = -pt->x * sin(rad) + pt->y * cos(rad);

                pt->x = x;
                pt->y = y;
            }

            template <typename T>
            inline int isea_tri_plane(int tri, isea_pt<T> *pt, T const& radius)
            {
                isea_pt<T> tc; /* center of triangle */

                if (downtri(tri)) {
                    isea_rotate(pt, 180.0);
                }
                tc = isea_triangle_xy<T>(tri);
                tc.x *= radius;
                tc.y *= radius;
                pt->x += tc.x;
                pt->y += tc.y;

                return tri;
            }

            /* convert projected triangle coords to quad xy coords, return quad number */
            template <typename T>
            inline int isea_ptdd(int tri, isea_pt<T> *pt)
            {
                int             downtri, quad;

                downtri = (((tri - 1) / 5) % 2 == 1);
                quad = ((tri - 1) % 5) + ((tri - 1) / 10) * 5 + 1;

                isea_rotate(pt, downtri ? 240.0 : 60.0);
                if (downtri) {
                    pt->x += 0.5;
                    /* pt->y += cos(30.0 * M_PI / 180.0); */
                    pt->y += .86602540378443864672;
                }
                return quad;
            }

            template <typename T>
            inline int isea_dddi_ap3odd(isea_dgg<T> *g, int quad, isea_pt<T> *pt, isea_pt<T> *di)
            {
                static T const pi = detail::pi<T>();

                isea_pt<T> v;
                T          hexwidth;
                T          sidelength;    /* in hexes */
                int        d, i;
                int        maxcoord;
                hex        h;

                /* This is the number of hexes from apex to base of a triangle */
                sidelength = (math::pow(T(2), g->resolution) + T(1)) / T(2);

                /* apex to base is cos(30deg) */
                hexwidth = cos(pi / 6.0) / sidelength;

                /* TODO I think sidelength is always x.5, so
                 * (int)sidelength * 2 + 1 might be just as good
                 */
                maxcoord = (int) (sidelength * 2.0 + 0.5);

                v = *pt;
                hexbin2(hexwidth, v.x, v.y, &h.x, &h.y);
                h.iso = 0;
                hex_iso(&h);

                d = h.x - h.z;
                i = h.x + h.y + h.y;

                /*
                 * you want to test for max coords for the next quad in the same
                 * "row" first to get the case where both are max
                 */
                if (quad <= 5) {
                    if (d == 0 && i == maxcoord) {
                        /* north pole */
                        quad = 0;
                        d = 0;
                        i = 0;
                    } else if (i == maxcoord) {
                        /* upper right in next quad */
                        quad += 1;
                        if (quad == 6)
                            quad = 1;
                        i = maxcoord - d;
                        d = 0;
                    } else if (d == maxcoord) {
                        /* lower right in quad to lower right */
                        quad += 5;
                        d = 0;
                    }
                } else if (quad >= 6) {
                    if (i == 0 && d == maxcoord) {
                        /* south pole */
                        quad = 11;
                        d = 0;
                        i = 0;
                    } else if (d == maxcoord) {
                        /* lower right in next quad */
                        quad += 1;
                        if (quad == 11)
                            quad = 6;
                        d = maxcoord - i;
                        i = 0;
                    } else if (i == maxcoord) {
                        /* upper right in quad to upper right */
                        quad = (quad - 4) % 5;
                        i = 0;
                    }
                }

                di->x = d;
                di->y = i;

                g->quad = quad;
                return quad;
            }

            template <typename T>
            inline int isea_dddi(isea_dgg<T> *g, int quad, isea_pt<T> *pt, isea_pt<T> *di)
            {
                isea_pt<T> v;
                T          hexwidth;
                int        sidelength;    /* in hexes */
                hex        h;

                if (g->aperture == 3 && g->resolution % 2 != 0) {
                    return isea_dddi_ap3odd(g, quad, pt, di);
                }
                /* todo might want to do this as an iterated loop */
                if (g->aperture >0) {
                    sidelength = (int) (math::pow(T(g->aperture), T(g->resolution / T(2))) + T(0.5));
                } else {
                    sidelength = g->resolution;
                }

                hexwidth = 1.0 / sidelength;

                v = *pt;
                isea_rotate(&v, -30.0);
                hexbin2(hexwidth, v.x, v.y, &h.x, &h.y);
                h.iso = 0;
                hex_iso(&h);

                /* we may actually be on another quad */
                if (quad <= 5) {
                    if (h.x == 0 && h.z == -sidelength) {
                        /* north pole */
                        quad = 0;
                        h.z = 0;
                        h.y = 0;
                        h.x = 0;
                    } else if (h.z == -sidelength) {
                        quad = quad + 1;
                        if (quad == 6)
                            quad = 1;
                        h.y = sidelength - h.x;
                        h.z = h.x - sidelength;
                        h.x = 0;
                    } else if (h.x == sidelength) {
                        quad += 5;
                        h.y = -h.z;
                        h.x = 0;
                    }
                } else if (quad >= 6) {
                    if (h.z == 0 && h.x == sidelength) {
                        /* south pole */
                        quad = 11;
                        h.x = 0;
                        h.y = 0;
                        h.z = 0;
                    } else if (h.x == sidelength) {
                        quad = quad + 1;
                        if (quad == 11)
                            quad = 6;
                        h.x = h.y + sidelength;
                        h.y = 0;
                        h.z = -h.x;
                    } else if (h.y == -sidelength) {
                        quad -= 4;
                        h.y = 0;
                        h.z = -h.x;
                    }
                }
                di->x = h.x;
                di->y = -h.z;

                g->quad = quad;
                return quad;
            }

            template <typename T>
            inline int isea_ptdi(isea_dgg<T> *g, int tri, isea_pt<T> *pt,
                                 isea_pt<T> *di)
            {
                isea_pt<T> v;
                int        quad;

                v = *pt;
                quad = isea_ptdd(tri, &v);
                quad = isea_dddi(g, quad, &v, di);
                return quad;
            }

            /* q2di to seqnum */
            template <typename T>
            inline int isea_disn(isea_dgg<T> *g, int quad, isea_pt<T> *di)
            {
                int             sidelength;
                int             sn, height;
                int             hexes;

                if (quad == 0) {
                    g->serial = 1;
                    return g->serial;
                }
                /* hexes in a quad */
                hexes = (int) (math::pow(T(g->aperture), T(g->resolution)) + T(0.5));
                if (quad == 11) {
                    g->serial = 1 + 10 * hexes + 1;
                    return g->serial;
                }
                if (g->aperture == 3 && g->resolution % 2 == 1) {
                    height = (int) (math::pow(T(g->aperture), T((g->resolution - 1) / T(2))));
                    sn = ((int) di->x) * height;
                    sn += ((int) di->y) / height;
                    sn += (quad - 1) * hexes;
                    sn += 2;
                } else {
                    sidelength = (int) (math::pow(T(g->aperture), T(g->resolution / T(2))) + T(0.5));
                    sn = (int) ((quad - 1) * hexes + sidelength * di->x + di->y + 2);
                }

                g->serial = sn;
                return sn;
            }

            /* TODO just encode the quad in the d or i coordinate
             * quad is 0-11, which can be four bits.
             * d' = d << 4 + q, d = d' >> 4, q = d' & 0xf
             */
            /* convert a q2di to global hex coord */
            template <typename T>
            inline int isea_hex(isea_dgg<T> *g, int tri, isea_pt<T> *pt,
                                isea_pt<T> *hex)
            {
                isea_pt<T> v;
#ifdef BOOST_GEOMETRY_PROJECTIONS_FIXME
                int sidelength;
                int d, i, x, y;
#endif // BOOST_GEOMETRY_PROJECTIONS_FIXME
                int quad;

                quad = isea_ptdi(g, tri, pt, &v);

                hex->x = ((int)v.x << 4) + quad;
                hex->y = v.y;

                return 1;
#ifdef BOOST_GEOMETRY_PROJECTIONS_FIXME
                d = (int)v.x;
                i = (int)v.y;

                /* Aperture 3 odd resolutions */
                if (g->aperture == 3 && g->resolution % 2 != 0) {
                    int offset = (int)(pow(T(3.0), T(g->resolution - 1)) + 0.5);

                    d += offset * ((g->quad-1) % 5);
                    i += offset * ((g->quad-1) % 5);

                    if (quad == 0) {
                        d = 0;
                        i = offset;
                    } else if (quad == 11) {
                        d = 2 * offset;
                        i = 0;
                    } else if (quad > 5) {
                        d += offset;
                    }

                    x = (2*d - i) /3;
                    y = (2*i - d) /3;

                    hex->x = x + offset / 3;
                    hex->y = y + 2 * offset / 3;
                    return 1;
                }

                /* aperture 3 even resolutions and aperture 4 */
                sidelength = (int) (pow(T(g->aperture), T(g->resolution / 2.0)) + 0.5);
                if (g->quad == 0) {
                    hex->x = 0;
                    hex->y = sidelength;
                } else if (g->quad == 11) {
                    hex->x = sidelength * 2;
                    hex->y = 0;
                } else {
                    hex->x = d + sidelength * ((g->quad-1) % 5);
                    if (g->quad > 5) hex->x += sidelength;
                    hex->y = i + sidelength * ((g->quad-1) % 5);
                }

                return 1;
#endif // BOOST_GEOMETRY_PROJECTIONS_FIXME
            }

            template <typename T>
            inline isea_pt<T> isea_forward(isea_dgg<T> *g, isea_geo<T> *in)
            {
                int        tri;
                isea_pt<T> out, coord;

                tri = isea_transform(g, in, &out);

                if (g->output == isea_addr_plane) {
                    isea_tri_plane(tri, &out, g->radius);
                    return out;
                }

                /* convert to isea standard triangle size */
                out.x = out.x / g->radius * isea_scale;
                out.y = out.y / g->radius * isea_scale;
                out.x += 0.5;
                out.y += 2.0 * .14433756729740644112;

                switch (g->output) {
                case isea_addr_projtri:
                    /* nothing to do, already in projected triangle */
                    break;
                case isea_addr_vertex2dd:
                    g->quad = isea_ptdd(tri, &out);
                    break;
                case isea_addr_q2dd:
                    /* Same as above, we just don't print as much */
                    g->quad = isea_ptdd(tri, &out);
                    break;
                case isea_addr_q2di:
                    g->quad = isea_ptdi(g, tri, &out, &coord);
                    return coord;
                    break;
                case isea_addr_seqnum:
                    isea_ptdi(g, tri, &out, &coord);
                    /* disn will set g->serial */
                    isea_disn(g, g->quad, &coord);
                    return coord;
                    break;
                case isea_addr_hex:
                    isea_hex(g, tri, &out, &coord);
                    return coord;
                    break;
                }

                return out;
            }
            /*
             * Proj 4 integration code follows
             */

            template <typename T>
            struct par_isea
            {
                isea_dgg<T> dgg;
            };

            // template class, using CRTP to implement forward/inverse
            template <typename T, typename Parameters>
            struct base_isea_spheroid
                : public base_t_f<base_isea_spheroid<T, Parameters>, T, Parameters>
            {
                par_isea<T> m_proj_parm;

                inline base_isea_spheroid(const Parameters& par)
                    : base_t_f<base_isea_spheroid<T, Parameters>, T, Parameters>(*this, par)
                {}

                // FORWARD(s_forward)
                // Project coordinates from geographic (lon, lat) to cartesian (x, y)
                inline void fwd(T const& lp_lon, T const& lp_lat, T& xy_x, T& xy_y) const
                {
                    isea_pt<T> out;
                    isea_geo<T> in;

                    in.lon = lp_lon;
                    in.lat = lp_lat;

                    isea_dgg<T> copy = this->m_proj_parm.dgg;
                    out = isea_forward(&copy, &in);

                    xy_x = out.x;
                    xy_y = out.y;
                }

                static inline std::string get_name()
                {
                    return "isea_spheroid";
                }

            };

            template <typename T>
            inline void isea_orient_init(srs::detail::proj4_parameters const& params,
                                         par_isea<T>& proj_parm)
            {
                std::string opt = pj_get_param_s(params, "orient");
                if (! opt.empty()) {
                    if (opt == std::string("isea")) {
                        isea_orient_isea(&proj_parm.dgg);
                    } else if (opt == std::string("pole")) {
                        isea_orient_pole(&proj_parm.dgg);
                    } else {
                        BOOST_THROW_EXCEPTION( projection_exception(error_ellipsoid_use_required) );
                    }
                }
            }

            template <typename T>
            inline void isea_orient_init(srs::dpar::parameters<T> const& params,
                                         par_isea<T>& proj_parm)
            {
                typename srs::dpar::parameters<T>::const_iterator
                    it = pj_param_find(params, srs::dpar::orient);
                if (it != params.end()) {
                    srs::dpar::value_orient o = static_cast<srs::dpar::value_orient>(it->template get_value<int>());
                    if (o == srs::dpar::orient_isea) {
                        isea_orient_isea(&proj_parm.dgg);
                    } else if (o == srs::dpar::orient_pole) {
                        isea_orient_pole(&proj_parm.dgg);
                    } else {
                        BOOST_THROW_EXCEPTION( projection_exception(error_ellipsoid_use_required) );
                    }
                }
            }

            template <typename T>
            inline void isea_mode_init(srs::detail::proj4_parameters const& params,
                                       par_isea<T>& proj_parm)
            {
                std::string opt = pj_get_param_s(params, "mode");
                if (! opt.empty()) {
                    if (opt == std::string("plane")) {
                        proj_parm.dgg.output = isea_addr_plane;
                    } else if (opt == std::string("di")) {
                        proj_parm.dgg.output = isea_addr_q2di;
                    } else if (opt == std::string("dd")) {
                        proj_parm.dgg.output = isea_addr_q2dd;
                    } else if (opt == std::string("hex")) {
                        proj_parm.dgg.output = isea_addr_hex;
                    } else {
                        BOOST_THROW_EXCEPTION( projection_exception(error_ellipsoid_use_required) );
                    }
                }
            }

            template <typename T>
            inline void isea_mode_init(srs::dpar::parameters<T> const& params,
                                       par_isea<T>& proj_parm)
            {
                typename srs::dpar::parameters<T>::const_iterator
                    it = pj_param_find(params, srs::dpar::mode);
                if (it != params.end()) {
                    srs::dpar::value_mode m = static_cast<srs::dpar::value_mode>(it->template get_value<int>());
                    if (m == srs::dpar::mode_plane) {
                        proj_parm.dgg.output = isea_addr_plane;
                    } else if (m == srs::dpar::mode_di) {
                        proj_parm.dgg.output = isea_addr_q2di;
                    } else if (m == srs::dpar::mode_dd) {
                        proj_parm.dgg.output = isea_addr_q2dd;
                    } else if (m == srs::dpar::mode_hex) {
                        proj_parm.dgg.output = isea_addr_hex;
                    } else {
                        BOOST_THROW_EXCEPTION( projection_exception(error_ellipsoid_use_required) );
                    }
                }
            }

            // Icosahedral Snyder Equal Area
            template <typename Params, typename T>
            inline void setup_isea(Params const& params, par_isea<T>& proj_parm)
            {
                std::string opt;

                isea_grid_init(&proj_parm.dgg);

                proj_parm.dgg.output = isea_addr_plane;
            /*        proj_parm.dgg.radius = par.a; / * otherwise defaults to 1 */
                /* calling library will scale, I think */

                isea_orient_init(params, proj_parm);

                pj_param_r<srs::spar::azi>(params, "azi", srs::dpar::azi, proj_parm.dgg.o_az);
                pj_param_r<srs::spar::lon_0>(params, "lon_0", srs::dpar::lon_0, proj_parm.dgg.o_lon);
                pj_param_r<srs::spar::lat_0>(params, "lat_0", srs::dpar::lat_0, proj_parm.dgg.o_lat);
                // TODO: this parameter is set below second time
                pj_param_i<srs::spar::aperture>(params, "aperture", srs::dpar::aperture, proj_parm.dgg.aperture);
                // TODO: this parameter is set below second time
                pj_param_i<srs::spar::resolution>(params, "resolution", srs::dpar::resolution, proj_parm.dgg.resolution);

                isea_mode_init(params, proj_parm);

                // TODO: pj_param_exists -> pj_get_param_b ?
                if (pj_param_exists<srs::spar::rescale>(params, "rescale", srs::dpar::rescale)) {
                    proj_parm.dgg.radius = isea_scale;
                }

                if (pj_param_i<srs::spar::resolution>(params, "resolution", srs::dpar::resolution, proj_parm.dgg.resolution)) {
                    /* empty */
                } else {
                    proj_parm.dgg.resolution = 4;
                }

                if (pj_param_i<srs::spar::aperture>(params, "aperture", srs::dpar::aperture, proj_parm.dgg.aperture)) {
                    /* empty */
                } else {
                    proj_parm.dgg.aperture = 3;
                }
            }

    }} // namespace detail::isea
    #endif // doxygen

    /*!
        \brief Icosahedral Snyder Equal Area projection
        \ingroup projections
        \tparam Geographic latlong point type
        \tparam Cartesian xy point type
        \tparam Parameters parameter type
        \par Projection characteristics
         - Spheroid
        \par Projection parameters
         - orient (string)
         - azi: Azimuth (or Gamma) (degrees)
         - lon_0: Central meridian (degrees)
         - lat_0: Latitude of origin (degrees)
         - aperture (integer)
         - resolution (integer)
         - mode (string)
         - rescale
        \par Example
        \image html ex_isea.gif
    */
    template <typename T, typename Parameters>
    struct isea_spheroid : public detail::isea::base_isea_spheroid<T, Parameters>
    {
        template <typename Params>
        inline isea_spheroid(Params const& params, Parameters const& par)
            : detail::isea::base_isea_spheroid<T, Parameters>(par)
        {
            detail::isea::setup_isea(params, this->m_proj_parm);
        }
    };

    #ifndef DOXYGEN_NO_DETAIL
    namespace detail
    {

        // Static projection
        BOOST_GEOMETRY_PROJECTIONS_DETAIL_STATIC_PROJECTION(srs::spar::proj_isea, isea_spheroid, isea_spheroid)

        // Factory entry(s)
        BOOST_GEOMETRY_PROJECTIONS_DETAIL_FACTORY_ENTRY_F(isea_entry, isea_spheroid)

        BOOST_GEOMETRY_PROJECTIONS_DETAIL_FACTORY_INIT_BEGIN(isea_init)
        {
            BOOST_GEOMETRY_PROJECTIONS_DETAIL_FACTORY_INIT_ENTRY(isea, isea_entry)
        }

    } // namespace detail
    #endif // doxygen

} // namespace projections

}} // namespace boost::geometry

#endif // BOOST_GEOMETRY_PROJECTIONS_ISEA_HPP


// __BEGIN_LICENSE__
// Copyright (C) 2006-2011 United States Government as represented by
// the Administrator of the National Aeronautics and Space Administration.
// All Rights Reserved.
// __END_LICENSE__

#include <vw/Image.h>
#include <vw/Plate/PlateFile.h>
#include <vw/Plate/TileManipulation.h>
using namespace vw;
using namespace vw::platefile;

#include <boost/foreach.hpp>
#include <boost/program_options.hpp>
namespace po = boost::program_options;

#include <boost/math/distributions/fisher_f.hpp>

using namespace std;

// --- Functions to Apply --------------------------

// Reduce Base class
//   defines interface
template <typename ImplT>
struct ReduceBase {
  inline ImplT& impl() { return static_cast<ImplT&>(*this); }
  inline ImplT const& impl() const { return static_cast<ImplT const&>(*this); }

  template <class PixelT>
  inline void operator()( list<ImageView<PixelT> > const& input,
                          list<TileHeader> const& input_header,
                          ImageView<PixelT> & output) {
    impl()(input,input_header,output);
  }
};

// Weighted Average Implementation
//   mimick this with own functions
struct WeightedAverage : public ReduceBase<WeightedAverage> {

  template <class PixelT>
  inline void operator()( list<ImageView<PixelT> > const& input,
                          list<TileHeader> const& /*input_header*/,
                          ImageView<PixelT> & output) {
    // Input Images will always have an alpha channel. That is a requirement of PlateFiles.
    int num_channels = CompoundNumChannels<PixelT>::value;
    std::vector<ImageView<float32> > sum_weighted_data;
    for ( uint8 i = 0; i < num_channels-1; i++ )
      sum_weighted_data.push_back(ImageView<float32>(input.front().cols(),
                                                     input.front().rows()));
    ImageView<float32> summed_weights(input.front().cols(),
                                      input.front().rows());

    // Summing multiple images
    typedef typename list< ImageView<PixelT > >::const_iterator image_iter;
    for ( image_iter image = input.begin();
          image != input.end(); image++ ) {
      summed_weights += channel_cast<float32>(select_channel(*image,num_channels-1));
      // Iterating over non alpha channels
      for ( uint8 i = 0; i < num_channels-1; i++ )
        sum_weighted_data[i] += channel_cast<float32>(select_channel(*image,num_channels-1))*channel_cast<float32>(select_channel(*image,i));
    }

    // Normalizing
    for ( uint8 i = 0; i < num_channels-1; i++ )
      sum_weighted_data[i] /= summed_weights;
    output.set_size(input.front().cols(),
                    input.front().rows());
    for ( uint8 i = 0; i < num_channels-1; i++ )
      select_channel(output,i) = sum_weighted_data[i];
    // Setting output alpha
    select_channel(output,num_channels-1) = threshold(summed_weights,0,
                                                      ChannelRange<typename PixelChannelType<PixelT>::type>::min(),
                                                      ChannelRange<typename PixelChannelType<PixelT>::type>::max());
  }
};

// Weighted Variance Implementation
struct WeightedVar2 : public ReduceBase<WeightedVar2> {

  template <class PixelT>
  inline void operator()( list<ImageView<PixelT> > const& input,
                          list<TileHeader> const& /*input_header*/,
                          ImageView<PixelT> & var2) {
    // Input Images will always have an alpha channel. That is a requirement of PlateFiles.
    int num_channels = CompoundNumChannels<PixelT>::value;
    std::vector<ImageView<float32> > sum_weighted_data; //store the mean
    std::vector<ImageView<float32> > sum_weighted_data2; //store the variance
    std::vector<ImageView<float32> > sum_weighted_stdv; //store the standard deviation

    for ( uint8 i = 0; i < num_channels-1; i++ ){
      sum_weighted_data.push_back(ImageView<float32>(input.front().cols(),
                                                     input.front().rows()));
      sum_weighted_data2.push_back(ImageView<float32>(input.front().cols(),
                                                     input.front().rows()));
      sum_weighted_stdv.push_back(ImageView<float32>(input.front().cols(),
                                                    input.front().rows()));
    }

    ImageView<float32> summed_weights(input.front().cols(),
                                      input.front().rows());

    // Summing multiple images
    typedef typename list< ImageView<PixelT > >::const_iterator image_iter;
    for ( image_iter image = input.begin();image != input.end(); image++ ) {
      summed_weights += channel_cast<float32>(select_channel(*image,num_channels-1));

      // Iterating over non alpha channels
      for ( uint8 i = 0; i < num_channels-1; i++ ){
        sum_weighted_data[i] += channel_cast<float32>(select_channel(*image,num_channels-1))*channel_cast<float32>(select_channel(*image,i));

        sum_weighted_data2[i] += channel_cast<float32>(select_channel(*image,num_channels-1))
                                 *channel_cast<float32>(select_channel(*image,i))*channel_cast<float32>(select_channel(*image,i));
      }
    }

    // Normalizing
    for ( uint8 i = 0; i < num_channels-1; i++ ){
      sum_weighted_data[i] /= summed_weights;
      sum_weighted_data2[i] = sum_weighted_data2[i]/summed_weights - sum_weighted_data[i]*sum_weighted_data[i];
      sum_weighted_stdv[i]=sqrt(clamp(sum_weighted_data2[i],0, 999999999));
    }

    var2.set_size(input.front().cols(),
                  input.front().rows());
    for ( uint8 i = 0; i < num_channels-1; i++ )
      select_channel(var2,i) = sum_weighted_stdv[i];
    // Setting output alpha
    select_channel(var2,num_channels-1) =
      threshold(summed_weights,0,
                ChannelRange<typename PixelChannelType<PixelT>::type>::min(),
                ChannelRange<typename PixelChannelType<PixelT>::type>::max());
  }
};

// Robust Mean Implementation
struct RobustMean : public ReduceBase<RobustMean> {
private:
  float smart_weighted_mean( vector<float> & weights,
                             vector<float> const& samples,
                             float const sign_level=0.3f,
                             float const learn_rate=0.1f,
                             float const error_tol=1e-5f,
                             int32 const max_iter=1000 ) {
    namespace bm = boost::math;
    switch ( samples.size() ) {
    case 1:
      weights[0]=1;
      return samples[0];
    case 2:
      weights[0]=1;
      weights[1]=1;
      return (samples[0]+samples[1])/2;
    default:

      float weighted_mean = 0;
      std::vector<float> prev_wt = weights; // the previous weights
      for (int i = 0; i < max_iter; ++i) {

        // accumulation of all sums
        float SW1 = 0;    // sum of weights
        float SW2 = 0;    // sum of squared weights
        float SWX = 0;    // weighted sum of samples
        float WX2 = 0;    // weighted sum of squared data
        for (size_t j = 0; j < samples.size(); ++j) {
          SW1 += weights[j];
          SW2 += weights[j]*weights[j];
          SWX += weights[j]*samples[j];
          WX2 += weights[j]*samples[j]*samples[j];
        }

        // weighted mean
        weighted_mean = SWX/SW1;

        float DN = SW1*WX2-SWX*SWX; // denominator
        float v2 = SW1-SW2/SW1;     // the second degree of freedom
        float sse_wt = 0;           // squared sum of weight differences
        for (size_t j = 0; j < samples.size(); ++j) {

          // F statistic
          float DF = samples[j]-weighted_mean; // difference from the mean
          float FS = (SW1*SW1-SW2)*DF*DF/DN;   // F statistic

          // degree of freedom
          float v1 = 1-weights[j]/SW1;         // the first degree of freedom

          // p-value calculation
          float p_value = 1;                   // p-value
          if (SW1 > 1 && DN > 0) {             // basic assumptions
            bm::fisher_f dist(v1,v2);
            p_value = boost::numeric_cast<float>(1-cdf(dist, FS));
          }

          // gradient decent update
          weights[j] += learn_rate*(p_value-sign_level);
          if ( weights[j] < 0 ) weights[j] = 0; // 0.0 is lower bound of weight
          if ( weights[j] > 1 ) weights[j] = 1; // 1.0 is upper bound of weight

          // squared sum of weight differences
          sse_wt += (weights[j]-prev_wt[j])*(weights[j]-prev_wt[j]);
          prev_wt[j] = weights[j];              // update previous weight

        }

        // terminal condition: mean squared difference of weights is
        // smaller than the tolerance
        if ( sse_wt/float(weights.size()) < error_tol ) break;
      }

      return  weighted_mean;
    }
  }

public:
  template <class PixelT>
  inline void operator()( list<ImageView<PixelT> > const& input,
                          list<TileHeader> const& /*input_header*/,
                          ImageView<PixelT> & output ) {
    uint8 num_v_channels = CompoundNumChannels<PixelT>::value-1;
    output.set_size( input.front().cols(),
                     input.front().rows() );

    // Iterating through every pixel within a tile
    for ( int32 ix = 0; ix < input.front().cols(); ix++ ) {
      for ( int32 iy = 0; iy < input.front().rows(); iy++ ) {
        vector<std::vector<float> > t_samples;
        t_samples.resize(num_v_channels);
        vector<float> t_weight;

        // Seperating data out into vectors, only loading up data that
        // is not empty.
        typedef typename list<ImageView<PixelT> >::const_iterator iter_type;
        for ( iter_type iter = input.begin();
              iter != input.end(); iter++ ) {
          if ( (*iter)(ix,iy)[num_v_channels] == 0 )
            continue;
          t_weight.push_back( float((*iter)(ix,iy)[num_v_channels])/float(ChannelRange<typename PixelChannelType<PixelT>::type>::max()) );
          for ( uint8 iz = 0; iz < num_v_channels; iz++ )
            t_samples[iz].push_back( (*iter)(ix,iy)[iz] );
        }

        if ( t_weight.empty() ) {
          output(ix,iy) = PixelT();
          continue;
        }

        typedef typename PixelChannelType<PixelT>::type ChannelT;
        output(ix,iy)[num_v_channels] = ChannelRange<ChannelT>::max();
        for ( uint8 iz = 0; iz < num_v_channels; iz++ ) {
          vector<float> copy = t_weight;
          output(ix,iy)[iz] =
            boost::numeric_cast<ChannelT>(smart_weighted_mean( copy,
                                                               t_samples[iz]));
        }
      } // iy - end loop
    }   // ix - first loop
  }     // end operator()
};

// --- Standard Terminal Argument ------------------

// Standard Arguments
struct Options {
  // Input
  Url url;
  int32 level;
  TransactionOrNeg start_trans_id, end_trans_id;
  std::string start_description;
  bool finish;

  // Output
  string function;
  TransactionOrNeg transaction_id;

  // For spawning multiple jobs
  int32 job_id, num_jobs;
};

void handle_arguments(int argc, char *argv[], Options& opt) {
  po::options_description general_options("Perform weighted averages of all layers within a tile inside a plate file");
  general_options.add_options()
    ("job_id,j", po::value(&opt.job_id)->default_value(0), "")
    ("num_jobs,n", po::value(&opt.num_jobs)->default_value(1), "")
    ("begin_transaction", po::value(&opt.start_trans_id)->default_value(0), "Input starting transaction ID range.")
    ("end_transaction", po::value(&opt.end_trans_id), "Input ending transaction ID range.")
    ("level,l", po::value(&opt.level)->default_value(-1), "Level inside the plate in which to process. -1 will error out and show the number of levels available.")
    ("function,f", po::value(&opt.function)->default_value("WeightedAvg"), "Functions that are available are [WeightedAvg RobustMean WeightedVar]")
    ("transaction-id,t",po::value(&opt.transaction_id)->default_value(2000), "Transaction id to write to")
    ("start", po::value(&opt.start_description), "Starts a multi-part plate reduce.")
    ("finish", po::bool_switch(&opt.finish)->default_value(false), "Finish a multi-part plate reduce")
    ("help,h", "Display this help message");

  po::options_description hidden_options("");
  hidden_options.add_options()
    ("input-file", po::value(&opt.url), "");

  po::options_description options("");
  options.add(general_options).add(hidden_options);

  po::positional_options_description p;
  p.add("input-file",-1);

  po::variables_map vm;
  try {
    po::store( po::command_line_parser( argc, argv ).options(options).positional(p).run(), vm );
    po::notify( vm );
  } catch (const po::error& e) {
    vw_throw( ArgumentErr() << "Error parsing input:\n\t"
              << e.what() << options );
  }

  std::ostringstream usage;
  usage << "Usage: " << argv[0] << " <plate_filename> [options]\n";

  if ( vm.count("help") || vm.count("input-file") != 1 || opt.transaction_id.newest())
    vw_throw( ArgumentErr() << usage.str() << general_options );
}

// --- Meta Application of Above Functions ----------

// apply_reduce
template <typename ReduceT, class PixelT>
void apply_reduce( boost::shared_ptr<PlateFile> platefile,
                   std::list<BBox2i> const& workunits,
                   Options& opt, ReduceBase<ReduceT>& reduce) {

  TerminalProgressCallback tpc("plate.platereduce", "Processing");
  double inc_tpc = 1.0/float(workunits.size());
  BOOST_FOREACH( const BBox2i& workunit, workunits) {
    tpc.report_incremental_progress(inc_tpc);
    for ( int ix = 0; ix < workunit.width(); ix++ ) {
      for ( int iy = 0; iy < workunit.height(); iy++ ) {
        Vector2i location(ix,iy);
        location += workunit.min();

        // Polling for Tiles
        std::list<TileHeader> tile_records;
        tile_records = platefile->search_by_location(location[0],
                                                     location[1],
                                                     opt.level,
                                                     TransactionRange(opt.start_trans_id, opt.end_trans_id));

        // No Tiles? No Problem!
        if (tile_records.empty())
          continue;

        // Loading images
        std::list<ImageView<PixelT> > tiles;
        BOOST_FOREACH( const TileHeader& tile, tile_records ) {
          ImageView<PixelT> new_tile;
          platefile->read( new_tile, location[0],
                           location[1], opt.level,
                           tile.transaction_id(), true );
          tiles.push_back(new_tile);
        }

        // Calling function
        ImageView<PixelT> result;
        reduce(tiles, tile_records, result);

        platefile->write_update(result, location[0], location[1], opt.level);
      }
    }
  }
  tpc.report_finished();
}

// Function that runs the apply_reduce over the plate file
template <typename ReduceT>
void do_run( Options& opt, ReduceBase<ReduceT>& reduce ) {
  boost::shared_ptr<PlateFile> platefile =
    boost::shared_ptr<PlateFile>( new PlateFile(opt.url) );

  if ( !opt.start_description.empty() ) {
    platefile->transaction_begin(opt.start_description, opt.transaction_id );
    vw_out() << "Transaction started with ID = " << platefile->transaction_id() << "\n";
    vw_out() << "Plate has " << platefile->num_levels() << " levels.\n";
    exit(0);
  }

  platefile->transaction_resume(opt.transaction_id.promote());

  if ( opt.finish ) {
    // Update the read cursor when the snapshot is complete!
    platefile->transaction_end(true);
    vw_out() << "Transaction " << opt.transaction_id << " complete.\n";
    exit(0);
  }

  if ( opt.level < 0 || opt.level >= boost::numeric_cast<int32>(platefile->num_levels()) ) {
    vw_throw( ArgumentErr() << "Incorrect level selection, "
              << opt.level << ".\n\nPlatefile " << opt.url.string() << " has "
              << platefile->num_levels() << " levels internally.\n" );
  }

  // This is arbitrary, just needed to divide up jobs
  int32 region_size = 1 << opt.level;
  BBox2i full_region(0,0,region_size,region_size);
  std::list<BBox2i> workunits = bbox_tiles(full_region,4,4);
  std::list<BBox2i> mworkunits;
  int32 count = 0;
  BOOST_FOREACH(const BBox2i& c, workunits) {
    if (count==opt.num_jobs)
      count=0;
    if (count==opt.job_id)
      mworkunits.push_back(c);
    count++;
  }
  vw_out() << "Job " << opt.job_id << "/" << opt.num_jobs << " has "
           << mworkunits.size() << " work units.\n";

  platefile->audit_log()
    << "Started multi-part reduce (t_id = " << opt.transaction_id
    << ") -- level:" << opt.level
    << " jobid:" << opt.job_id << "/" << opt.num_jobs << "\n";

  platefile->write_request();

  switch(platefile->pixel_format()) {
  case VW_PIXEL_GRAYA:
    switch(platefile->channel_type()) {
    case VW_CHANNEL_UINT8:
      apply_reduce<ReduceT, PixelGrayA<uint8> >(platefile, mworkunits,
                                                opt, reduce);
      break;
    case VW_CHANNEL_INT16:
      apply_reduce<ReduceT, PixelGrayA<int16> >(platefile, mworkunits,
                                                opt, reduce);
      break;
    case VW_CHANNEL_FLOAT32:
      apply_reduce<ReduceT, PixelGrayA<float32> >(platefile, mworkunits,
                                                  opt, reduce);
      break;
    default:
      vw_throw(InputErr() << "Platefile contains unsupported channel type.\n" );
    }
    break;
  case VW_PIXEL_RGBA:
    switch(platefile->channel_type()) {
    case VW_CHANNEL_UINT8:
      apply_reduce<ReduceT, PixelRGBA<uint8> >(platefile, mworkunits,
                                               opt, reduce);
      break;
    default:
      vw_throw(InputErr() << "Platefile contains unsupported channel type.\n" );
    }
    break;
  default:
    vw_throw(InputErr() << "Platefile contains a pixel type thats unsupported.\n" );
  }

  platefile->write_complete();
  platefile->audit_log()
    << "Finished multi-part reduce (t_id = " << opt.transaction_id
    << ") -- level:" << opt.level
    << " jobid:" << opt.job_id << "/" << opt.num_jobs << "\n";
}

int main( int argc, char *argv[] ) {

  Options opt;
  try {
    handle_arguments( argc, argv, opt );

    // Handing out jobs now
    boost::to_lower(opt.function);
    if ( opt.function == "weightedavg" ) {
      WeightedAverage f;
      do_run<WeightedAverage>( opt, f );
    } else if ( opt.function == "robustmean" ) {
      RobustMean f;
      do_run<RobustMean>( opt, f );
    }
    else if ( opt.function == "weightedvar" ) {
      WeightedVar2 f;
      do_run<WeightedVar2>( opt, f );
    } else {
      vw_throw( ArgumentErr() << "Unknown function, " << opt.function << "\n" );
    }

  } catch ( const ArgumentErr& e ) {
    vw_out() << e.what() << std::endl;
    return 1;
  } catch ( const Exception& e ) {
    std::cerr << "Error: " << e.what() << std::endl;
    return 1;
  }

  return 0;
}