/***************************** LICENSE START ***********************************

 Copyright 2021 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

// MvGeoPoints.cc,   apr03/vk


#include "MvGeoPoints.h"

#include <algorithm>
#include <fstream>
#include <sstream>
#include <string>

#include "fstream_mars_fix.h"

#include "MvLocation.h"
#include "MvMiscellaneous.h"
#include "mars.h"
#include "Tokenizer.h"

// the precision with which we write geopoints values
#define VALUE_PRECISION (10)

std::vector<std::string> MvGeoPointColumnInfo::usedColNames() const
{
    // get the complete list of column names, and then remove those that
    // are not actually used (possible: elevation)
    std::vector<std::string> cnames(colNames_);
    if (!hasElevations_) {
        cnames.erase(std::remove_if(cnames.begin(), cnames.end(),
                                    [](std::string colname) { return colname == "elevation"; }),
                     cnames.end());
    }
    return cnames;
}

std::vector<eGeoColType> MvGeoPointColumnInfo::usedColTypes() const
{
    // get the complete list of column types, and then remove those that
    // are not actually used (possible: elevation)
    std::vector<eGeoColType> ctypes(colTypes_);
    if (!hasElevations_) {
        ctypes.erase(std::remove_if(ctypes.begin(), ctypes.end(),
                                    [](eGeoColType coltype) { return coltype == eGeoColElevation; }),
                     ctypes.end());
    }
    return ctypes;
}

bool MvGeoPointColumnInfo::isCompatibleForMerging(const MvGeoPointColumnInfo& in)
{
    if (nvalcols_ != in.nvalcols_)
        return false;
    if (nvalcolsforcompute_ != in.nvalcolsforcompute_)
        return false;
    if (usedColNames() != in.usedColNames())
        return false;
    if (usedColTypes() != in.usedColTypes())
        return false;

    return true;
}

void MvGeoPointColumnInfo::ensureOnlyOneValueColumn()
{
    // resize the column arrays to have only one elment, 'value'
    nvalcols_ = 1;
    nvalcolsforcompute_ = 1;
    colTypes_.resize(ncoordcols_ + nvalcols_);
    colNames_.resize(ncoordcols_);
    colNames_.push_back("value");
}


//_____________________________________________________________________
bool MvGeoPoints::extract(const char* line, size_t r)
{
    char buf[255];  //-- for numeric/string check
    double d_latitude;
    double d_longitude;
    double d_height;

    set_rowIndex(r);  // all params will be set on this row
    set_strValue("");

    std::istringstream myInput(line);
    if (gfmt_ == eGeoXYV)  //-- XYV is 'X_lon,Y _lat, Val' format
    {
        myInput >> d_longitude >> d_latitude >> buf;

        _stringOrNumber(buf);  // here it would not make sense to update the gobal format
                               // as eGeoString is assumed to have 6 columns

        set_lat_y(d_latitude);
        set_lon_x(d_longitude);
        set_height(0);
        set_date(0);
        set_time(0);
    }
    else if (gfmt_ != eGeoNCols) {
        double d_date;  //-- in case user has floating point valued dates
        double d_time;
        double d_val1;
        double d_val2;

        if (hasVector())  //-- polar or XY vector?
        {
            myInput >> d_latitude >> d_longitude >> d_height >> d_date >> d_time >> d_val1 >> d_val2;
            set_value(d_val1);
            set_value2(d_val2);
        }
        else  //-- TRADITIONAL: Lat/Lon/lev/dat/tim/Val
        {
            myInput >> d_latitude >> d_longitude >> d_height >> d_date >> d_time >> buf;

            _stringOrNumber(buf);

            // if (gfmt_ == eGeoString)  // update the 'global' geo format?
            //     geoFmt = eGeoString;
        }

        set_lat_y(d_latitude);
        set_lon_x(d_longitude);
        set_height(d_height);
        set_date((long)d_date);
        set_time((long)d_time);
        // value1/2 will have already been set by _stringOrNumber, or in the hasVector() part above
    }
    else  // ncols
    {
        double d_date;  //-- in case user has floating point valued dates
        double d_time;
        double d_elevation;
        std::string s_string;

        // read the co-ordinate values
        for (size_t c = 0; c < nCoordCols(); c++) {
            eGeoColType t = colType(c);
            if (t == eGeoColStnId) {
                myInput >> s_string;
                s_string = metview::stationIdFromFile(s_string);
                set_strValue(s_string);
            }
            else if (t == eGeoColLat) {
                myInput >> d_latitude;
                set_lat_y(d_latitude);
            }
            else if (t == eGeoColLon) {
                myInput >> d_longitude;
                set_lon_x(d_longitude);
            }
            else if (t == eGeoColLevel) {
                myInput >> d_height;
                set_height(d_height);
            }
            else if (t == eGeoColElevation) {
                myInput >> d_elevation;
                set_elevation(d_elevation);
            }
            else if (t == eGeoColDate) {
                myInput >> d_date;
                set_date((long)d_date);
            }
            else if (t == eGeoColTime) {
                myInput >> d_time;
                set_time((long)d_time);
            }
        }

        // read the 'value' values
        size_t vi = 0;
        double d;
        while (myInput >> d) {
            set_ivalue(d, vi++);
        }
        myInput.clear();  // myInput will be in an error state by the end of the row

        // first time round, we store the number of columns; subsequently, we check that all rows have the same number of columns
        if (nValCols() == 0) {
            nValCols(vi);
            nValColsForCompute(nValCols());
        }
        else {
            if (nValCols() != vi) {
                marslog(LOG_EROR, "Geopoints file has different numbers of columns: the first offending row is:");
                marslog(LOG_EROR, "%s", line);
                return false;
            }
        }
    }

    return !myInput.fail();
}


//_____________________________________________________________________
void MvGeoPoints::_stringOrNumber(char* buf)
{
    bool isNumeric = true;
    int dcnt = 0;
    char* p = buf;

    if (*p == '-' || *p == '+')  //-- sign is OK
        ++p;

    if (*p && isalpha(*p)) {
        isNumeric = false;  //-- cannot be a number
    }
    else {
        dcnt = _countDigits(p);      //-- (leading) digits?
        if (dcnt == 0 && *p != '.')  //-- 0 digits => only decimal point is OK
            isNumeric = false;

        if (isNumeric && *p == '.') {
            ++p;
            dcnt += _countDigits(p);  //-- trailing digits?
            if (dcnt == 0)
                isNumeric = false;  //-- decimal point without digits
        }

        if (isNumeric && (*p == 'e' || *p == 'E')) {
            ++p;
            if (*p == '-' || *p == '+')  //-- exponent sign is OK
                ++p;
            if (_countDigits(p) == 0)
                isNumeric = false;  //-- digits must follow
        }

        if (isNumeric && *p && isgraph(*p))
            isNumeric = false;  //-- must not follow by a printable char
    }

    if (isNumeric) {
        set_value(atof(buf));  //-- is numeric: convert!
    }
    else {
        set_strValue(buf);  //-- is string: copy!
        set_value(0);
        set_format(eGeoString);
    }
}

//_____________________________________________________________________
int MvGeoPoints::_countDigits(char*& p)
{
    int dcnt = 0;

    while (p && *p) {
        if (isdigit(*p))
            ++dcnt;
        else
            break;

        ++p;
    }

    return dcnt;
}


std::string
MvGeoPoints::column(size_t row, size_t col, MvGeoPointColumnInfo& colinfo, int& type)
{
    size_t totalcols = colinfo.ncoordcols_ + colinfo.nvalcols_;
    type = eGeoVDouble;

    if (col < 0 || col >= totalcols)
        return "BAD COLUMN INDEX";

    // is the column index one of the value columns?
    eGeoColType et = colinfo.colTypes_[col];
    if (!MvGeoPoints::colTypeIsCoord(et)) {
        col -= colinfo.ncoordcols_;  // index into value columns now
        if (col < (size_t)colinfo.nvalcols_) {
            return std::to_string(ivalue(row, col));
        }
    }
    else {
        // no, it's a coordinate column

        switch (et) {
            case eGeoColStnId:
                type = eGeoVString;
                return strValue(row);
            case eGeoColLat:
                return std::to_string(lat_y(row));
            case eGeoColLon:
                return std::to_string(lon_x(row));
            case eGeoColLevel:
                return std::to_string(height(row));
            case eGeoColElevation:
                return std::to_string(elevation(row));
            case eGeoColDate: {
                type = eGeoVLong;
                return std::to_string(date(row));
            }
            case eGeoColTime: {
                type = eGeoVLong;
                return std::to_string(time(row));
            }
            default:
                return "UNKNOWN";
        }
    }

    return std::string("FORMAT NOT DEFINED");
}


//_____________________________________________________________________
void MvGeoPoints::location(double lat, double lon)
{
    set_lat_y(lat);
    if (lat != GEOPOINTS_MISSING_VALUE) {
        if (lat > 90) {
            marslog(LOG_INFO, "Geopoint latitude value %g forced to be 90", lat_y());
            set_lat_y(90);
        }
        if (lat_y() < -90) {
            marslog(LOG_INFO, "Geopoint latitude value %g forced to be -90", lat_y());
            set_lat_y(-90);
        }
    }

    if (lon != GEOPOINTS_MISSING_VALUE) {
        while (lon < -180)
            lon += 360;
        while (lon > 360)
            lon -= 360;
    }
    set_lon_x(lon);
}


bool MvGeoPoints::isLocationValid() const
{
    return (lat_y() <= 90. && lat_y() >= -90. && lon_x() <= 360. && lon_x() >= -360.);
}

// -- MvGeop1Writer
// -- Tiny utility class to enable the writing of a single geopoint to file.
// -- It stores a reference to an MvGeoPoints and a row index.
// -- The original implementation was for the MvGeoP1 class, which has now
// -- been removed, so it is possible that there is now a better way to do this.

class MvGeop1Writer
{
    //! Friend function to write one point (one line) into a MvGeoPoints file
    friend std::ostream& operator<<(std::ostream& aStream, const MvGeop1Writer& gpw);

public:
    MvGeop1Writer(MvGeoPoints& gpIn, size_t r) :
        gp_(gpIn),
        r_(r) {}

    MvGeoPoints& gp_;
    size_t r_;
};


//_____________________________________________________________________

std::ostream& operator<<(std::ostream& aStream, const MvGeop1Writer& gpw)
{
    MvGeoPoints& gp = gpw.gp_;
    const char cSeparator[] = "\t";


    // I (IR) don't know why this code was here, but originally, a space character
    // was being written to the start of each value line. I have now (July 2018)
    // removed this, for time and file size efficiency.

    // const char cStartOfLine[] = " ";
    // aStream << cStartOfLine;


    // General note about precision settings: we must be careful if a
    // user-callable function to set the precision is created. This is
    // because we need to ensure that missing values are still correctly
    // written and read. See the value defined for missing values in
    // MvGeoPoints.h to see how many decimal places are required for
    // faithful reading and writing of missing values.
    // See also MvGeoPoints::write, as this also uses the precision value.


    int myOldPrec = aStream.precision();  //-- store current floating point precision
    aStream.precision(7);                 //-- default of 6 digits may not be enough


    gp.set_rowIndex(gpw.r_);

    if (gp.format() == eGeoXYV) {
        aStream << gp.lon_x() << cSeparator
                << gp.lat_y() << cSeparator;
    }
    else if (gp.format() == eGeoNCols) {
        for (size_t c = 0; c < gp.ncols(); c++) {
            switch (gp.colType(c)) {
                case eGeoColLat:
                    aStream << gp.lat_y() << cSeparator;
                    break;
                case eGeoColLon:
                    aStream << gp.lon_x() << cSeparator;
                    break;
                case eGeoColLevel:
                    aStream << gp.height() << cSeparator;
                    break;
                case eGeoColElevation:
                    if (gp.hasElevations())
                        aStream << gp.elevation() << cSeparator;
                    break;
                case eGeoColDate:
                    aStream << gp.date() << cSeparator;
                    break;
                case eGeoColTime:
                    aStream << gp.time() << cSeparator;
                    break;
                case eGeoColStnId:
                    if (gp.hasStnIds())
                        aStream << metview::stationIdForWritingToFile(gp.strValue()) << cSeparator;
                    break;
                default:
                    break;
            }
        }
    }
    else {
        aStream << gp.lat_y() << cSeparator
                << gp.lon_x() << cSeparator
                << gp.height() << cSeparator
                << gp.date() << cSeparator
                << gp.time() << cSeparator;
    }

    aStream.precision(VALUE_PRECISION);  //-- value may need even more precision

    switch (gp.format()) {
        case eGeoTraditional:
        case eGeoXYV:
            aStream << gp.value();
            break;

        case eGeoString:
            aStream << gp.strValue().c_str();
            break;

        case eGeoVectorPolar:
        case eGeoVectorXY:
            aStream << gp.speed() << cSeparator
                    << gp.direc();
            break;

        case eGeoNCols:
            size_t i;
            for (i = 0; i < gp.nValCols() - 1; i++)
                aStream << gp.ivalue(i) << cSeparator;

            aStream << gp.ivalue(i);  // last value without the separator
            break;

        default:
            break;
    }

    aStream.precision(myOldPrec);  //-- revert back to original precision

    return aStream;
}


//_____________________________________________________________________
//_____________________________________________________________________
//_________________________MvGeoPoints_________________________________
//_____________________________________________________________________
//_____________________________________________________________________


//_____________________________________________________________________
// return the coordinate column map - if it's the first time, populate it
// - this is a static function, and a static member variable

std::map<std::string, eGeoColType> MvGeoPoints::coordColMap_;

const std::map<std::string, eGeoColType>& MvGeoPoints::coordColMap()
{
    if (coordColMap_.empty()) {
        coordColMap_["latitude"] = eGeoColLat;
        coordColMap_["longitude"] = eGeoColLon;
        coordColMap_["level"] = eGeoColLevel;
        coordColMap_["elevation"] = eGeoColElevation;
        coordColMap_["date"] = eGeoColDate;
        coordColMap_["time"] = eGeoColTime;
        coordColMap_["stnid"] = eGeoColStnId;
        coordColMap_["value"] = eGeoColValue;
        coordColMap_["value2"] = eGeoColValue2;
    }
    return coordColMap_;
}

eGeoColType MvGeoPoints::colTypeFromName(const std::string& name, bool failIfUnknown)
{
    auto& colmap = coordColMap();
    auto it = colmap.find(name);
    if (it == colmap.end())
        if (failIfUnknown)
            return eGeoColError;
        else
            return eGeoColValue;
    else
        return it->second;
}


bool MvGeoPoints::colTypeIsCoord(eGeoColType t)
{
    return (t != eGeoColValue && t != eGeoColValue2 && t != eGeoColError);
}


//_____________________________________________________________________

MvGeoPoints::MvGeoPoints(size_t count, int numvals, eGeoFormat efmt, bool init) :
    gfmt_(efmt),
    count_(count),
    path_("/file/name/not/given"),
    vi_(0),
    r_(0),
    dbSystem_(""),
    dbPath_("")
{
    this->setColumnsForFormat();
    metadata_.clear();
    newReservedSize(count, init);
    if (init)
        set_format(efmt, numvals);
}


//_____________________________________________________________________

MvGeoPoints::MvGeoPoints(size_t count, const MvGeoPointColumnInfo& colInfo, eGeoFormat efmt, bool init) :
    MvGeoPoints(count, colInfo.nvalcols_, efmt, init)  // C++11
{
    // do the same as the above constructor, but also copy across all the column information
    colInfo_ = colInfo;
}


//_____________________________________________________________________

MvGeoPoints::MvGeoPoints(size_t ncount, bool init) :
    gfmt_(eGeoTraditional),
    count_(ncount),
    path_("/file/name/not/given"),
    vi_(0),
    r_(0),
    dbSystem_(""),
    dbPath_("")
{
    newReservedSize(count());
    if (!init) {
        newReservedSize(count());
        setColumnsForFormat();
        metadata_.clear();
    }
}

//_____________________________________________________________________

MvGeoPoints::MvGeoPoints(const MvGeoPoints& gp)
{
    copy(gp);
}

//_____________________________________________________________________

MvGeoPoints::MvGeoPoints(const char* name, const size_t nmax) :
    count_(0),
    vi_(0),
    r_(0)
{
    set_path(name);
    load(nmax);
}

//_____________________________________________________________________

MvGeoPoints::~MvGeoPoints() = default;
//_____________________________________________________________________
void MvGeoPoints::copy(const MvGeoPoints& gp, eGeoCopyMode mode)
{
    unload();

    gfmt_ = gp.format();
    count_ = gp.count();
    sgfmt_ = gp.format();
    colInfo_ = gp.colInfo_;
    vi_ = 0;
    r_ = 0;

    if (mode == eGeoCopyAll) {
        dbSystem_ = gp.dbSystem();
        dbColumn_ = gp.dbColumn();
        dbColumnAlias_ = gp.dbColumnAlias();
        dbPath_ = gp.dbPath();
        dbQuery_ = gp.dbQuery();
        metadata_ = gp.metadataConst();
    }
    else {
        colInfo_.ensureOnlyOneValueColumn();
    }

    if (count() > 0) {
        strings_ = gp.strings_;
        latitudes_ = gp.latitudes_;
        longitudes_ = gp.longitudes_;
        heights_ = gp.heights_;
        elevations_ = gp.elevations_;
        dates_ = gp.dates_;
        times_ = gp.times_;
        bool copyAll = (mode == eGeoCopyAll);
        if (copyAll) {
            values_ = gp.values_;
        }
        else {
            resizeValueColumns();
        }
    }
    else {
        unload();
    }
}


void MvGeoPoints::copyRow(const MvGeoPoints& src, size_t srcRow, size_t destRow)
{
    strings_[destRow] = src.strings_[srcRow];  // XXX do we always need to copy this?
    latitudes_[destRow] = src.latitudes_[srcRow];
    longitudes_[destRow] = src.longitudes_[srcRow];
    heights_[destRow] = src.heights_[srcRow];
    elevations_[destRow] = src.elevations_[srcRow];
    dates_[destRow] = src.dates_[srcRow];
    times_[destRow] = src.times_[srcRow];

    for (size_t i = 0; i < nValCols(); i++) {
        values_[i][destRow] = src.values_[i][srcRow];
    }
}


// optimised version of copyRow for copying multiple rows in one go
void MvGeoPoints::copyRows(const MvGeoPoints& src, size_t srcRowFirst, size_t numRows, size_t destRow)
{
    std::copy(std::begin(src.strings_) + srcRowFirst,
              std::begin(src.strings_) + srcRowFirst + numRows,
              std::begin(strings_) + destRow);

    std::copy(std::begin(src.latitudes_) + srcRowFirst,
              std::begin(src.latitudes_) + srcRowFirst + numRows,
              std::begin(latitudes_) + destRow);

    std::copy(std::begin(src.longitudes_) + srcRowFirst,
              std::begin(src.longitudes_) + srcRowFirst + numRows,
              std::begin(longitudes_) + destRow);

    std::copy(std::begin(src.heights_) + srcRowFirst,
              std::begin(src.heights_) + srcRowFirst + numRows,
              std::begin(heights_) + destRow);

    std::copy(std::begin(src.elevations_) + srcRowFirst,
              std::begin(src.elevations_) + srcRowFirst + numRows,
              std::begin(elevations_) + destRow);

    std::copy(std::begin(src.dates_) + srcRowFirst,
              std::begin(src.dates_) + srcRowFirst + numRows,
              std::begin(dates_) + destRow);

    std::copy(std::begin(src.times_) + srcRowFirst,
              std::begin(src.times_) + srcRowFirst + numRows,
              std::begin(times_) + destRow);

    for (size_t i = 0; i < nValCols(); i++) {
        std::copy(std::begin(src.values_[i]) + srcRowFirst,
                  std::begin(src.values_[i]) + srcRowFirst + numRows,
                  std::begin(values_[i]) + destRow);
    }
}


//_____________________________________________________________________
bool MvGeoPoints::areRowsEqual(size_t r1, size_t r2)
{
    // if (gfmt_ != in.gfmt_)
    //     return false;
    if (lat_y(r1) != lat_y(r2))
        return false;
    if (lon_x(r1) != lon_x(r2))
        return false;
    if (height(r1) != height(r2))
        return false;
    if (elevation(r1) != elevation(r2))
        return false;
    if (date(r1) != date(r2))
        return false;
    if (time(r1) != time(r2))
        return false;
    if (strValue(r1) != strValue(r2))
        return false;
    for (size_t c = 0; c < values_.size(); c++) {
        if (ivalue(r1, c) != ivalue(r2, c))
            return false;
    }

    return true;
}


//_____________________________________________________________________
MvGeoPoints&
MvGeoPoints::operator=(const MvGeoPoints& gp)
{
    if (&gp == this)
        return *this;

    unload();
    copy(gp);

    return *this;
}


//_____________________________________________________________________
void MvGeoPoints::resizeValueColumns()
{
    if ((nValCols()) > 0 && (nValCols() != values_.size()))
        values_.resize(nValCols());

    for (auto& value : values_)
        value.resize(count_);
}


//_____________________________________________________________________
void MvGeoPoints::newReservedSize(size_t size, bool /*init*/)
{
    unload();
    set_count(size);

    strings_.resize(count_);
    latitudes_.resize(count_);
    longitudes_.resize(count_);
    heights_.resize(count_);
    elevations_.resize(count_);
    dates_.resize(count_);
    times_.resize(count_);

    resizeValueColumns();

    /*
    if (init)
        pts_.resize(count_);  // calls constructors
    else
        pts_.reserve(count_);  // just allocates memory*/
}


//_____________________________________________________________________
// Needed for GeoSubsampleFunction in bufr.cc - we can't just use the '<'
// operator, because it also compares the value, which we do not care about
// in GeoSubsampleFunction
bool MvGeoPoints::latLonHeightBefore(const MvGeoPoints& in, size_t r_this, size_t r_in) const
{
    if (lat_y(r_this) != in.lat_y(r_in))
        return lat_y(r_this) > in.lat_y(r_in);  //-- from North to South

    if (lon_x(r_this) != in.lon_x(r_in))
        return lon_x(r_this) < in.lon_x(r_in);  //-- from West to East

    if (height(r_this) != in.height(r_in))
        return height(r_this) < in.height(r_in);

    if (elevation(r_this) != in.elevation(r_in))
        return elevation(r_this) < in.elevation(r_in);
    return false;
}


//_____________________________________________________________________
bool MvGeoPoints::sameLocation(const MvGeoPoints& in, size_t r_this, size_t r_in)
{
    return (lat_y(r_this) == in.lat_y(r_in) && lon_x(r_this) == in.lon_x(r_in) && height(r_this) == in.height(r_in) &&
            elevation(r_this) == in.elevation(r_in)) &&
           (!latlon_missing(r_this) && !in.latlon_missing(r_in));
}


//_____________________________________________________________________
void MvGeoPoints::set_format(eGeoFormat fmt, size_t numvals)
{
    set_format(fmt);
    setColumnsForFormat();

    if (format() == eGeoNCols)
        nValCols(numvals);


    // When we change the format of geopoints, we may need to resize the values array

    if ((fmt != eGeoNCols) && (gfmt_ == fmt))  // fast return if no change needed
        return;

    gfmt_ = fmt;

    // for backwards compatibility, we allocate 2 values even for the 1-valued
    // formats. In theory, someone could create a standard geopoints variable
    // and set its value2 array from a macro, and later query it

    size_t nvalcols = 2;  // how many values (data values, not lat/lon, etc)
    if (gfmt_ == eGeoNCols)
        nvalcols = numvals;

    /*
    size_t nvals = 1;  // how many values (data values, not lat/lon, etc)

    if (gfmt_ == eGeoVectorPolar || gfmt_ == eGeoVectorXY)
        nvals = 2;
*/

    if (nvalcols != values_.size())
        values_.resize(nvalcols);
}

//_____________________________________________________________________
bool MvGeoPoints::load(const char* path)
{
    if (path_ != path && count_ > 0)
        unload();  //-- unload if different data exist

    path_ = path;

    return load();
}

//_____________________________________________________________________
bool MvGeoPoints::load(const size_t /*nmax*/)
{
    if (count_)
        return true;

    std::ifstream f(path_.c_str());
    if (!f) {
        marslog(LOG_EROR, "Could not open geopoints file: %s", path_.c_str());
        return false;
    }

    return load(f);
}

void MvGeoPoints::addColName(std::string name, bool markStnIdAsUsed, bool markElevationAsUsed, bool addToFront)
{
    if (addToFront)
        colInfo_.colNames_.insert(colInfo_.colNames_.begin(), name);
    else
        colInfo_.colNames_.push_back(name);
    eGeoColType colType = colTypeFromName(name);
    addColType(colType, addToFront);
    if (markStnIdAsUsed && (colType == eGeoColStnId))
        hasStnIds(true);
    if (markElevationAsUsed && (colType == eGeoColElevation))
        hasElevations(true);
}


void MvGeoPoints::addColType(eGeoColType t, bool addToFront)
{
    if (addToFront)
        colInfo_.colTypes_.insert(colInfo_.colTypes_.begin(), t);
    else
        colInfo_.colTypes_.push_back(t);
}


//_____________________________________________________________________
// The line should be something like this:
// stnid   lat   time  long    date
// and can contain, optionally, the names of the value columns, e.g.
// stnid   lat   time  long    date  temperature  ozone risk_factor
// returns false if there was an error in parsing the line
bool MvGeoPoints::parseColumnNames(char* line)
{
    // populate colNames_, ncols_ and nvalcols_

    // tokenise into a list of strings
    std::string sbuf(line);
    std::vector<std::string> sv;
    Tokenizer parse(" \t");
    parse(sbuf, sv);

    // for each string on the line
    colInfo_.ncoordcols_ = 0;
    bool valcols = false;  // co-ordinate cols first, then value cols
    clearColNames();
    clearColTypes();

    for (auto& name : sv) {
        addColName(name, true, true);

        // is this a standard co-ordinate column name?
        eGeoColType colType = colTypeFromName(name, true);
        if (colTypeIsCoord(colType)) {
            if (valcols) {
                marslog(LOG_EROR, "Error parsing geopoints #COLUMNS line: all co-ordinate columns must come before the value columns - %s", name.c_str());
                return false;
            }
            colInfo_.ncoordcols_++;
        }
        else  // no, it must be a user-defined value name
        {
            valcols = true;
        }
    }


    // check that the essential columns exist
    if (std::find(colInfo_.colTypes_.begin(), colInfo_.colTypes_.end(), eGeoColLat) == colInfo_.colTypes_.end() ||
        std::find(colInfo_.colTypes_.begin(), colInfo_.colTypes_.end(), eGeoColLon) == colInfo_.colTypes_.end()) {
        marslog(LOG_EROR, "NCOLS-based geopoints must contain latitude and longitude columns");
        return false;
    }

    return true;
}

//_____________________________________________________________________
// Count how many value columns there are on the first data line
// - we can't just take the number of value header strings, because we
// do not require all value columns to have names
int MvGeoPoints::countValueColumns(char* line, int numCoordCols)
{
    // tokenise into a list of strings
    std::string sbuf(line);
    std::vector<std::string> sv;
    Tokenizer parse(" \t");
    parse(sbuf, sv);
    return (int)sv.size() - numCoordCols;
}


//_____________________________________________________________________
// If we have unnamed value columns, set their names to be empty strings
void MvGeoPoints::fillValueColumnNames()
{
    int numUnnamedCols = totalcols() - colNames().size();
    for (int i = 0; i < numUnnamedCols; i++) {
        char buf[128];
        sprintf(buf, "_v%d", i + 1);  // first unnamed col is _v1, next is _v2
        addColName(std::string(buf));
    }
}


void MvGeoPoints::ensureNColsHasStnIds()
{
    // if NCOLS format, and no stnid, then add to front of list
    if (gfmt_ == eGeoNCols &&
        (std::find(colInfo_.colTypes_.begin(), colInfo_.colTypes_.end(), eGeoColStnId) == colInfo_.colTypes_.end())) {
        addColName("stnid", false, false, true);
        colInfo_.ncoordcols_++;
    }
}


//_____________________________________________________________________
bool MvGeoPoints::load(std::ifstream& f, const size_t nmax)
{
    char line[10240];
    size_t n = 0;
    int numPts = 0;
    std::streampos sp = f.tellg();


    if (nmax == 0) {
        //-- first count the lines
        while (f.getline(line, sizeof(line)))
            numPts++;
    }
    else
        numPts = nmax;

    unload();

    f.clear();
    f.seekg(sp);
    // f.seekg(0, std::ios::beg);

    gfmt_ = eGeoTraditional;

    bool metadata = false;
    bool db_info = false;
    bool db_query = false;
    bool colnames = false;

    while (f.getline(line, sizeof(line))) {
        if (strncmp(line, "#DATA", 5) == 0) {
            break;
        }
        else if (strncmp(line, "#FORMAT ", 8) == 0) {
            const char* fp = line + 7;
            while (fp && *fp == ' ')
                ++fp;

            if (strncmp(fp, "POLAR_VECTOR", 12) == 0) {
                gfmt_ = eGeoVectorPolar;  //-- polar vector extension
            }
            else if (strncmp(fp, "XY_VECTOR", 9) == 0) {
                gfmt_ = eGeoVectorXY;  //-- cartesian vector extension
            }
            else if (strncmp(fp, "XYV", 3) == 0) {
                gfmt_ = eGeoXYV;  //-- "French" extension
            }
            else if (strncmp(fp, "LLV", 3) == 0) {
                gfmt_ = eGeoXYV;  //-- old name for XYV
            }
            else if (strncmp(fp, "NCOLS", 3) == 0) {
                gfmt_ = eGeoNCols;  //-- flexible format with unlimited columns for values
            }
            else {
                marslog(LOG_EROR, "Unknown geopoints format: %s", fp);
            }

            // Set format info
            this->setColumnsForFormat();
        }

        else if (strncmp(line, "#COLUMNS", 7) == 0) {
            colnames = true;  // the column names will be on the next line
        }

        else if (colnames) {
            if (!parseColumnNames(line)) {
                marslog(LOG_EROR, "Error parsing geopoints column names");
                return false;
            }
            colnames = false;
        }


        else if (strncmp(line, "#METADATA", 9) == 0)  // start of meta-data
        {
            metadata = true;
            db_info = false;
        }

        else if (metadata == true)  // within the meta-data block
        {
            std::string sbuf(line);
            std::vector<std::string> sv;
            Tokenizer parse("=");
            parse(sbuf, sv);
            if (sv.size() == 2)
                metadata_[sv[0]] = sv[1];  // store this line of metadata
        }


        // Information about the database, query etc. that
        // generated the geopoints file
        else if (strncmp(line, "#DB_INFO ", 8) == 0) {
            db_info = true;
            metadata = false;
        }

        else if (db_info == true && strstr(line, "DB_SYSTEM:") != 0) {
            std::string sbuf(line);
            std::string::size_type pos = sbuf.find("DB_SYSTEM:");
            sbuf = sbuf.substr(pos + 10);
            dbSystem_ = sbuf;
        }

        else if (db_info == true && strstr(line, "DB_COLUMN:") != 0) {
            std::string sbuf(line);
            std::string::size_type pos = sbuf.find("DB_COLUMN:");
            sbuf = sbuf.substr(pos + 10);
            std::vector<std::string> sv;

            Tokenizer parse(";");
            parse(sbuf, sv);

            if (gfmt_ == eGeoTraditional && sv.size() == 6) {
                dbColumn_["lat"] = sv[0];
                dbColumn_["lon"] = sv[1];
                dbColumn_["level"] = sv[2];
                dbColumn_["date"] = sv[3];
                dbColumn_["time"] = sv[4];
                dbColumn_["value"] = sv[5];
            }
            else if (gfmt_ == eGeoXYV && sv.size() == 3) {
                dbColumn_["lon"] = sv[0];
                dbColumn_["lat"] = sv[1];
                dbColumn_["value"] = sv[2];
            }
            else if ((gfmt_ == eGeoVectorPolar || gfmt_ == eGeoVectorXY) && sv.size() == 7) {
                dbColumn_["lat"] = sv[0];
                dbColumn_["lon"] = sv[1];
                dbColumn_["level"] = sv[2];
                dbColumn_["date"] = sv[3];
                dbColumn_["time"] = sv[4];
                dbColumn_["value"] = sv[5];
                dbColumn_["value2"] = sv[6];
            }
        }

        else if (db_info == true && strstr(line, "DB_COLUMN_ALIAS:") != 0) {
            std::string sbuf(line);
            std::string::size_type pos = sbuf.find("DB_COLUMN_ALIAS:");
            sbuf = sbuf.substr(pos + 16);

            std::vector<std::string> sv;
            Tokenizer parse(";");
            parse(sbuf, sv);

            if (gfmt_ == eGeoTraditional && sv.size() == 6) {
                dbColumnAlias_["lat"] = sv[0];
                dbColumnAlias_["lon"] = sv[1];
                dbColumnAlias_["level"] = sv[2];
                dbColumnAlias_["date"] = sv[3];
                dbColumnAlias_["time"] = sv[4];
                dbColumnAlias_["value"] = sv[5];
            }
            else if (gfmt_ == eGeoXYV && sv.size() == 3) {
                dbColumnAlias_["lon"] = sv[0];
                dbColumnAlias_["lat"] = sv[1];
                dbColumnAlias_["value"] = sv[2];
            }
            else if ((gfmt_ == eGeoVectorPolar || gfmt_ == eGeoVectorXY) && sv.size() == 7) {
                dbColumnAlias_["lat"] = sv[0];
                dbColumnAlias_["lon"] = sv[1];
                dbColumnAlias_["level"] = sv[2];
                dbColumnAlias_["date"] = sv[3];
                dbColumnAlias_["time"] = sv[4];
                dbColumnAlias_["value"] = sv[5];
                dbColumnAlias_["value2"] = sv[6];
            }
        }

        else if (db_info == true && strstr(line, "DB_PATH:") != 0) {
            std::string sbuf(line);
            std::string::size_type pos = sbuf.find("DB_PATH:");
            sbuf = sbuf.substr(pos + 8);
            dbPath_ = sbuf;
        }

        else if (db_info == true && strstr(line, "DB_QUERY_BEGIN") != 0) {
            db_query = true;
        }

        else if (db_info == true && db_query == true) {
            dbQuery_.push_back(line);
        }
        else if (strstr(line, "DB_QUERY_END") != 0) {
            db_query = true;
        }
    }

    // Set format info
    if (gfmt_ == eGeoTraditional && sgfmt_.empty()) {
        this->setColumnsForFormat();
    }

    newReservedSize(numPts);

    // Read data
    if (nmax == 0) {
        while (f.getline(line, sizeof(line))) {
            if ((*line != '#') && (strlen(line) > 4)) {
                if (n == 0 && gfmt_ == eGeoNCols) {
                    // first time only - count the number of values
                    nValCols(0);
                    nValCols(countValueColumns(line, colInfo_.ncoordcols_));
                    fillValueColumnNames();
                }

                if (!extract(line, n)) {
                    marslog(LOG_EROR, "Error parsing geopoints file %s at value line %d", path_.c_str(), n + 1);
                    return false;
                }
                n++;
            }
            else if (!strncmp(line, "#GEO", 4))  // start of new geopoints file
            {
                break;
            }
        }
    }
    else {
        for (size_t i = 0; i < nmax; i++) {
            if (!f.getline(line, sizeof(line))) {
                marslog(LOG_EROR, "Geopoints file has less data than expected: %s", path_.c_str());
                return false;
            }

            if ((*line != '#') && (strlen(line) > 4)) {
                if (!extract(line, i)) {
                    marslog(LOG_EROR, "Error parsing geopoints file %s at value line %d", path_.c_str(), n + 1);
                    return false;
                }
                n++;
            }
        }
    }

    ensureNColsHasStnIds();
    // compute the no of value cols here
    if (n == 0 && gfmt_ == eGeoNCols) {
        nValCols(ncols() - nCoordCols());
    }
    set_count(n);

    return true;
}

//_____________________________________________________________________
void MvGeoPoints::unload()
{
    strings_.destroyVals();
    latitudes_.destroyVals();
    longitudes_.destroyVals();
    heights_.destroyVals();
    elevations_.destroyVals();
    dates_.destroyVals();
    times_.destroyVals();

    for (auto& value : values_)
        value.destroyVals();

    set_count(0);
}

//_____________________________________________________________________
bool MvGeoPoints::write(const char* filename)
{
    int nPreviousPrecision;

    std::ofstream fout(filename);
    if (!fout) {
        marslog(LOG_EROR, "Unable to open geopoints file for writing: %s", filename);
        return false;
    }

    fout << "#GEO\n";

    switch (gfmt_) {
        case eGeoVectorPolar:
            fout << "#FORMAT POLAR_VECTOR\n"
                 << "# lat\tlon\theight\tdate\t\ttime\tspeed\tdirection\n";
            break;

        case eGeoVectorXY:
            fout << "#FORMAT XY_VECTOR\n"
                 << "# lat\tlon\theight\tdate\t\ttime\tu\tv\n";
            break;

        case eGeoXYV:
            fout << "#FORMAT XYV\n"
                 << "# lon-x\tlat-y\tvalue\n";
            break;

        case eGeoNCols:
            fout << "#FORMAT NCOLS\n"
                 << "#COLUMNS\n";
            for (size_t i = 0; i < colInfo_.colNames_.size(); i++) {
                // don't write out the stnid column if there are no station ids / similar for elevations
                if (colInfo_.colTypes_[i] == eGeoColStnId && !colInfo_.hasStnIds_)
                    continue;
                if (colInfo_.colTypes_[i] == eGeoColElevation && !hasElevations())
                    continue;
                fout << colInfo_.colNames_[i].c_str() << "\t";
            }
            fout << "\n";
            break;

        default:
            //-- this is for both eGeoTraditional and eGeoString
            //-- no "#FORMAT" line is needed
            fout << "# lat\tlon\theight\tdate\t\ttime\tvalue\n";
            break;
    }


    // Insert a line that will tell the user which value represents missing points.
    // Take care not to disturb the floating-point precision, but we need to use
    // the correct one that will actually be used in the file.
    // Note that the storing and restoring of the original precision value
    // in the output stream is probably unnecessary, but it is done just once
    // per geopoints file and so should be insignificant and allows the
    // implementation of the << operator on a single geopoint to be changed
    // without unexpected side-effects.

    nPreviousPrecision = fout.precision();
    fout.precision(VALUE_PRECISION);
    fout << "# Missing values represented by " << GEOPOINTS_MISSING_VALUE
         << " (not user-changeable)" << std::endl;
    fout.precision(nPreviousPrecision);


    // metadata, if there is any

    const metadata_t md = metadataConst();
    if (!md.empty()) {
        fout << "#METADATA" << std::endl;
        auto it = md.begin();
        while (it != md.end()) {
            std::string key = it->first;
            MvVariant val = it->second;
            fout << key << "=" << val.toString() << std::endl;
            it++;
        }
    }

    // start the data section

    fout << "#DATA" << std::endl;

    MvGeop1Writer gpw(*this, 0);

    for (size_t r = 0; r < count_; ++r) {
        gpw.r_ = r;
        fout << gpw
             << std::endl;
    }

    return true;
}

/*
//_____________________________________________________________________
MvGeoP1
MvGeoPoints::nearestPoint(double lat_y, double lon_x) const
{
    if (count_ == 0)
        return MvGeoP1();  //-- should we...

    MvLocation myInputLoc(lat_y, lon_x);
    MvLocation myFirstLoc(pts_[0].lat_y(), pts_[0].lon_x());

    double myShortestDist = myInputLoc.distanceInMeters(myFirstLoc);
    long myNearestPi      = 0;

    for (int p = 1; p < count_; ++p) {
        MvLocation myCurrentLoc(pts_[p].lat_y(), pts_[p].lon_x());
        double myCurrentDist = myInputLoc.distanceInMeters(myCurrentLoc);

        if (myCurrentDist < myShortestDist) {
            myShortestDist = myCurrentDist;
            myNearestPi    = p;
        }
    }

    return pts_[myNearestPi];
}
*/

//_____________________________________________________________________
// MvGeoPoints::indexOfFirstValidPoint
// Returns the index of the first geopoint that is valid in the set.
// If none are valid, then -1 is returned.
// Note that this function only considers the first value in each
// geopoint, ignoring value2.

size_t MvGeoPoints::indexOfFirstValidPoint(size_t c) const
{
    for (size_t r = 0; r < count_; r++) {
        if (!value_missing(r, c)) {
            return r;
        }
    }

    // if we got to here, then there are no valid points
    return INVALID_INDEX;
}


//_____________________________________________________________________
// MvGeoPoints::sort()
// Sorts points geographically - from North to South, West to East


class MvGeoPointIndex
{
public:
    MvGeoPointIndex(size_t index, MvGeoPoints* gpt);
    bool operator<(const MvGeoPointIndex& rhs) const;

    double latitude() const { return gpt_->lat_y(index_); }
    double longitude() const { return gpt_->lon_x(index_); }
    double height() const { return gpt_->height(index_); }
    double elevation() const { return gpt_->elevation(index_); }
    double value() const { return gpt_->value(index_); }

    size_t index_;
    MvGeoPoints* gpt_;
};


MvGeoPointIndex::MvGeoPointIndex(size_t index, MvGeoPoints* gpt) :
    index_(index),
    gpt_(gpt)
{
}


//_____________________________________________________________________
// Used in the sort() function. We need to compare the values as well, otherwise
// the remove_duplicates() function will not work correctly (it compares values
// of adjacent points, so if there are multiple co-located points with different
// values, they might not be detected as duplicates unless the sorting takes
// the values into account).
bool MvGeoPointIndex::operator<(const MvGeoPointIndex& rhs) const
{
    // performance profiling revealed that the latitude() function was costly here when
    // handling large datasets, so reduce the number of calls by saving the results

    double lat1 = latitude();
    double lat2 = rhs.latitude();
    if (lat1 != lat2)
        return lat1 > lat2;  //-- from North to South

    double lon1 = longitude();
    double lon2 = rhs.longitude();
    if (lon1 != lon2)
        return lon1 < lon2;  //-- from West to East

    if (height() != rhs.height())
        return height() < rhs.height();

    if (elevation() != rhs.elevation())
        return elevation() < rhs.elevation();

    if (value() != rhs.value())
        return value() < rhs.value();

    return false;
}


//_____________________________________________________________________


void MvGeoPoints::sort()
{
    if (count() < 2)  //-- no need to sort if empty or only one point
        return;


    MvGeoPoints tempCopy(*this);


    //-- to make sort faster for huge files, copy input geopoints into
    //-- several latitude band lists;
    //-- here we define the width and the number of these latitude bands
    const double cLatBandSize = 1.0;
    const int cLatBandCount = (int)(180.0 / cLatBandSize) + 1;

    //-- STL provides tools for sorting
    std::vector<std::vector<MvGeoPointIndex> > LatListVec;
    std::vector<MvGeoPointIndex> emptyList;
    LatListVec.assign(cLatBandCount + 1, emptyList);

    //-- first coarse distribution into STL lists that are stored in STL vector
    for (size_t s = 0; s < count(); ++s) {
        int band = cLatBandCount - int((lat_y(s) + 90.5) / cLatBandSize);

        //-- if invalid latitude band value then sort into head or tail
        if (band < 0)
            band = 0;
        else if (band > cLatBandCount)
            band = cLatBandCount;

        LatListVec[band].push_back(MvGeoPointIndex(s, &tempCopy));
    }

    std::vector<MvGeoPointIndex> work;
    work.reserve(count_);  // reserve so that we don't call default constructor unnecessarily

    //-- sort each latitude band STL list and copy to output
    for (int vecList = 0; vecList < cLatBandCount + 1; ++vecList) {
        std::vector<MvGeoPointIndex> curList = LatListVec[vecList];
        if (!curList.empty()) {
            std::sort(curList.begin(), curList.end());

            for (auto& p : curList) {
                work.push_back(p);
            }

            curList.clear();
        }
    }

    // put the points indexed by 'work' back into the result
    for (size_t r = 0; r < count(); r++) {
        copyRow(tempCopy, work[r].index_, r);
    }
}

//_____________________________________________________________________
void MvGeoPoints::removeDuplicates()
{
    if (count() > 0) {
        sort();

        MvGeoPoints tempCopy(*this);

        size_t curr = 0;  //-- store now in case there is just 1 point
        size_t prev = 0;  //-- store the first point
        size_t dest = 0;  //-- next index to store non-duplicate point in

        for (size_t p = 1; p < count_; ++p)  //-- start from the second point
        {
            curr = p;  //-- current geopoint
            if (!tempCopy.areRowsEqual(curr, prev)) {
                copyRow(tempCopy, prev, dest++);  //-- points non-equal => copy prev
            }
            prev = curr;  //-- store current as previous
        }

        copyRow(tempCopy, curr, dest++);  //-- last point cannot be duplicate

        set_count(dest);  //-- adjust current point count

        // the following line removed at the request of Mark Rodwell
        // marslog(LOG_INFO, "MvGeoPoints::removeDuplicates: %d duplicates removed", iRem);
    }
}
//_____________________________________________________________________
void MvGeoPoints::offset(double latOffset, double lonOffset)
{
    if (count() > 0) {
        for (size_t r = 0; r < count_; ++r) {
            set_rowIndex(r);
            double actualLatOffset = (lat_y() == GEOPOINTS_MISSING_VALUE) ? 0.0 : latOffset;
            double actualLonOffset = (lon_x() == GEOPOINTS_MISSING_VALUE) ? 0.0 : lonOffset;
            location(lat_y() + actualLatOffset, lon_x() + actualLonOffset);
        }
    }
}

int MvGeoPoints::indexOfNamedValue(std::string& name)
{
    // e.g. if there are 4 co-ordinate columns, then the first value column index is 4 (0-based index)
    // which will be index 0 into the values array, so we always need to subtract the number of co-ordinate
    // columns from the result
    size_t index = std::find(colInfo_.colNames_.begin(), colInfo_.colNames_.end(), name) - colInfo_.colNames_.begin();
    if (index < colInfo_.colNames_.size())
        return index - colInfo_.ncoordcols_;
    else
        return -1;
}

std::vector<std::string> MvGeoPoints::valueColNames() const
{
    std::vector<std::string> result;
    for (size_t i = 0; i < colInfo_.colTypes_.size(); i++) {
        eGeoColType colType = colInfo_.colTypes_[i];
        if (!colTypeIsCoord(colType)) {
            std::string name = colName(i);
            result.push_back(name);
        }
    }
    return result;
}

std::vector<std::string> MvGeoPoints::usedColNames() const
{
    // get the complete list of column names, and then remove those that
    // are not actually used (possible: elevation)
    std::vector<std::string> cnames(colNames());
    if ((format() == eGeoNCols) && !hasElevations()) {
        cnames.erase(std::remove_if(cnames.begin(), cnames.end(),
                                    [](std::string colname) { return colname == "elevation"; }),
                     cnames.end());
    }
    return cnames;
}

void MvGeoPoints::setColumnsForFormat()
{
    // Clean the structure
    if (colInfo_.colNames_.size())
        clearColNames();

    if (colInfo_.colTypes_.size())
        colInfo_.colTypes_.clear();

    colInfo_.ncoordcols_ = 5;          // should be 5, but 4 will give us numvals=2 for backwards compatibility
    colInfo_.nvalcolsforcompute_ = 1;  // all formats except xy_vector and ncols only operate on one column
    hasStnIds(false);                  // assume they're not there unless we read them or the user sets them
    hasElevations(false);              // assume they're not there unless we read them or the user sets them


    if (gfmt_ == eGeoTraditional || gfmt_ == eGeoString) {
        sgfmt_ = "Traditional";
        colInfo_.ncols_ = 6;
        nValCols(1);
        colInfo_.colNames_.reserve(colInfo_.ncols_);
        addColName("latitude");
        addColName("longitude");
        addColName("level");
        addColName("date");
        addColName("time");
        addColName("value");
    }
    else if (gfmt_ == eGeoXYV) {
        sgfmt_ = "XYV";
        colInfo_.ncols_ = 3;
        nValCols(1);
        colInfo_.ncoordcols_ = 2;
        colInfo_.colNames_.reserve(colInfo_.ncols_);
        addColName("longitude");
        addColName("latitude");
        addColName("value");
    }
    else if (gfmt_ == eGeoVectorPolar) {
        sgfmt_ = "Polar_Vector";
        colInfo_.ncols_ = 7;
        nValCols(2);
        colInfo_.nvalcolsforcompute_ = 1;
        colInfo_.colNames_.reserve(colInfo_.ncols_);
        addColName("latitude");
        addColName("longitude");
        addColName("level");
        addColName("date");
        addColName("time");
        addColName("magnitude");
        addColName("angle");
    }
    else if (gfmt_ == eGeoVectorXY) {
        sgfmt_ = "XY_Vector";
        colInfo_.ncols_ = 7;
        nValCols(2);
        colInfo_.nvalcolsforcompute_ = 2;
        colInfo_.colNames_.reserve(colInfo_.ncols_);
        addColName("latitude");
        addColName("longitude");
        addColName("level");
        addColName("date");
        addColName("time");
        addColName("x-comp");
        addColName("y-comp");
    }

    else if (gfmt_ == eGeoNCols) {
        sgfmt_ = "NCols";
        colInfo_.ncols_ = 7;
        nValCols(0);
        colInfo_.ncoordcols_ = 7;
        colInfo_.colNames_.reserve(colInfo_.ncols_);
        addColName("stnid");
        addColName("latitude");
        addColName("longitude");
        addColName("level");
        addColName("date");
        addColName("time");
        addColName("elevation");
    }


    return;
}

std::string MvGeoPoints::value(size_t row, size_t col, int& type)
{
    return column(row, col, colInfo_, type);
}
