/******************************************************************************
 * Project:  ogr linear referencing utility
 * Purpose:  main source file
 * Author:   Dmitry Baryshnikov (aka Bishop), polimax@mail.ru
 *
 ******************************************************************************
 * Copyright (C) 2014 NextGIS
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"

#include "commonutils.h"
#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_string.h"
#include "gdal_version.h"
#include "gdal.h"
#include "gdal_alg.h"
#include "ogr_api.h"
#include "ogr_p.h"
#include "ogrsf_frmts.h"
#include "gdalargumentparser.h"

#include <limits>
#include <map>
#include <set>
#include <vector>

#define FIELD_START "beg"
#define FIELD_FINISH "end"
#define FIELD_SCALE_FACTOR "scale"
constexpr double DELTA = 0.00000001;  // - delta
#ifdef HAVE_GEOS
constexpr double TOLERANCE_DEGREE = 0.00008983153;
constexpr double TOLERANCE_METER = 10.0;
#endif

enum operation
{
    op_unknown = 0,
    op_create,
    op_get_pos,
    op_get_coord,
    op_get_subline
};

typedef struct _curve_data
{
    OGRLineString *pPart;
    double dfBeg, dfEnd, dfFactor;

    bool IsInside(const double &dfDist) const
    {
        return (dfDist + DELTA >= dfBeg) && (dfDist - DELTA <= dfEnd);
    }
} CURVE_DATA;

/************************************************************************/
/*                         SetupTargetLayer()                           */
/************************************************************************/

static OGRLayer *SetupTargetLayer(OGRLayer *poSrcLayer, GDALDataset *poDstDS,
                                  char **papszLCO, const char *pszNewLayerName,
                                  const char *pszOutputSepFieldName = nullptr)
{
    const CPLString szLayerName =
        pszNewLayerName == nullptr ? CPLGetBasename(poDstDS->GetDescription())
                                   : pszNewLayerName;

    /* -------------------------------------------------------------------- */
    /*      Get other info.                                                 */
    /* -------------------------------------------------------------------- */
    OGRFeatureDefn *poSrcFDefn = poSrcLayer->GetLayerDefn();

    /* -------------------------------------------------------------------- */
    /*      Find requested geometry fields.                                 */
    /* -------------------------------------------------------------------- */

    OGRSpatialReference *poOutputSRS = poSrcLayer->GetSpatialRef();

    /* -------------------------------------------------------------------- */
    /*      Find the layer.                                                 */
    /* -------------------------------------------------------------------- */

    // GetLayerByName() can instantiate layers that would have been
    // 'hidden' otherwise, for example, non-spatial tables in a
    // PostGIS-enabled database, so this apparently useless command is
    // not useless... (#4012)
    CPLPushErrorHandler(CPLQuietErrorHandler);
    OGRLayer *poDstLayer = poDstDS->GetLayerByName(szLayerName);
    CPLPopErrorHandler();
    CPLErrorReset();

    if (poDstLayer != nullptr)
    {
        const int nLayerCount = poDstDS->GetLayerCount();
        int iLayer = -1;  // Used after for.
        for (iLayer = 0; iLayer < nLayerCount; iLayer++)
        {
            OGRLayer *poLayer = poDstDS->GetLayer(iLayer);
            if (poLayer == poDstLayer)
                break;
        }

        if (iLayer == nLayerCount)
            // Should not happen with an ideal driver.
            poDstLayer = nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      If the layer does not exist, then create it.                    */
    /* -------------------------------------------------------------------- */
    if (poDstLayer == nullptr)
    {
        if (!poDstDS->TestCapability(ODsCCreateLayer))
        {
            fprintf(stderr,
                    _("Layer %s not found, and "
                      "CreateLayer not supported by driver.\n"),
                    szLayerName.c_str());
            return nullptr;
        }

        OGRwkbGeometryType eGType = wkbLineString;

        CPLErrorReset();

        if (poDstDS->TestCapability(ODsCCreateGeomFieldAfterCreateLayer))
        {
            eGType = wkbNone;
        }

        poDstLayer = poDstDS->CreateLayer(
            szLayerName, poOutputSRS, static_cast<OGRwkbGeometryType>(eGType),
            papszLCO);

        if (poDstLayer == nullptr)
            return nullptr;

        if (poDstDS->TestCapability(ODsCCreateGeomFieldAfterCreateLayer))
        {
            OGRGeomFieldDefn oGFldDefn(poSrcFDefn->GetGeomFieldDefn(0));
            if (poOutputSRS != nullptr)
                oGFldDefn.SetSpatialRef(poOutputSRS);
            oGFldDefn.SetType(wkbLineString);
            poDstLayer->CreateGeomField(&oGFldDefn);
        }
    }

    /* -------------------------------------------------------------------- */
    /*      Otherwise we will append to it, if append was requested.        */
    /* -------------------------------------------------------------------- */
    else
    {
        fprintf(stderr, _("FAILED: Layer %s already exists.\n"),
                szLayerName.c_str());
        return nullptr;
    }

    // Create beg, end, scale factor fields.
    OGRFieldDefn oFieldDefn_Beg(FIELD_START, OFTReal);
    if (poDstLayer->CreateField(&oFieldDefn_Beg) != OGRERR_NONE)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Create %s field failed!",
                 oFieldDefn_Beg.GetNameRef());
        return nullptr;
    }

    OGRFieldDefn oFieldDefn_End(FIELD_FINISH, OFTReal);
    if (poDstLayer->CreateField(&oFieldDefn_End) != OGRERR_NONE)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Create %s field failed!",
                 oFieldDefn_End.GetNameRef());
        return nullptr;
    }

    OGRFieldDefn oFieldDefn_SF(FIELD_SCALE_FACTOR, OFTReal);
    if (poDstLayer->CreateField(&oFieldDefn_SF) != OGRERR_NONE)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Create %s field failed!",
                 oFieldDefn_SF.GetNameRef());
        return nullptr;
    }

    if (pszOutputSepFieldName != nullptr)
    {
        OGRFieldDefn oSepField(pszOutputSepFieldName, OFTString);
        oSepField.SetWidth(254);
        if (poDstLayer->CreateField(&oSepField) != OGRERR_NONE)
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Create %s field failed!",
                     oSepField.GetNameRef());
            return nullptr;
        }
    }

    // Now that we've created a field, GetLayerDefn() won't return NULL.
    OGRFeatureDefn *poDstFDefn = poDstLayer->GetLayerDefn();

    // Sanity check: if it fails, the driver is buggy.
    if (poDstFDefn != nullptr && poDstFDefn->GetFieldCount() != 3)
    {
        CPLError(CE_Warning, CPLE_AppDefined,
                 "The output driver has claimed to have added the %s field, "
                 "but it did not!",
                 oFieldDefn_Beg.GetNameRef());
    }

    return poDstLayer;
}

/* -------------------------------------------------------------------- */
/*                  CheckDestDataSourceNameConsistency()                */
/* -------------------------------------------------------------------- */

static void CheckDestDataSourceNameConsistency(const char *pszDestFilename,
                                               const char *pszDriverName)
{
    char *pszDestExtension = CPLStrdup(CPLGetExtension(pszDestFilename));

    // TODO: Would be good to have driver metadata like for GDAL drivers.
    static const char *apszExtensions[][2] = {{"shp", "ESRI Shapefile"},
                                              {"dbf", "ESRI Shapefile"},
                                              {"sqlite", "SQLite"},
                                              {"db", "SQLite"},
                                              {"mif", "MapInfo File"},
                                              {"tab", "MapInfo File"},
                                              {"s57", "S57"},
                                              {"bna", "BNA"},
                                              {"csv", "CSV"},
                                              {"gml", "GML"},
                                              {"kml", "KML"},
                                              {"kmz", "LIBKML"},
                                              {"json", "GeoJSON"},
                                              {"geojson", "GeoJSON"},
                                              {"dxf", "DXF"},
                                              {"gdb", "FileGDB"},
                                              {"pix", "PCIDSK"},
                                              {"sql", "PGDump"},
                                              {"gtm", "GPSTrackMaker"},
                                              {"gmt", "GMT"},
                                              {"pdf", "PDF"},
                                              {nullptr, nullptr}};
    static const char *apszBeginName[][2] = {{"PG:", "PG"},
                                             {"MySQL:", "MySQL"},
                                             {"CouchDB:", "CouchDB"},
                                             {"GFT:", "GFT"},
                                             {"MSSQL:", "MSSQLSpatial"},
                                             {"ODBC:", "ODBC"},
                                             {"OCI:", "OCI"},
                                             {"SDE:", "SDE"},
                                             {"WFS:", "WFS"},
                                             {nullptr, nullptr}};

    for (int i = 0; apszExtensions[i][0] != nullptr; i++)
    {
        if (EQUAL(pszDestExtension, apszExtensions[i][0]) &&
            !EQUAL(pszDriverName, apszExtensions[i][1]))
        {
            fprintf(stderr,
                    _("Warning: The target file has a '%s' extension, "
                      "which is normally used by the %s driver,\n"
                      "but the requested output driver is %s. "
                      "Is it really what you want ?\n"),
                    pszDestExtension, apszExtensions[i][1], pszDriverName);
            break;
        }
    }

    for (int i = 0; apszBeginName[i][0] != nullptr; i++)
    {
        if (EQUALN(pszDestFilename, apszBeginName[i][0],
                   strlen(apszBeginName[i][0])) &&
            !EQUAL(pszDriverName, apszBeginName[i][1]))
        {
            fprintf(stderr,
                    _("Warning: The target file has a name which is normally "
                      "recognized by the %s driver,\n"
                      "but the requested output driver is %s. "
                      "Is it really what you want ?\n"),
                    apszBeginName[i][1], pszDriverName);
            break;
        }
    }

    CPLFree(pszDestExtension);
}

//------------------------------------------------------------------------
// AddFeature
//------------------------------------------------------------------------

static OGRErr AddFeature(OGRLayer *const poOutLayer, OGRLineString *pPart,
                         double dfFrom, double dfTo, double dfScaleFactor,
                         bool bQuiet,
                         const char *pszOutputSepFieldName = nullptr,
                         const char *pszOutputSepFieldValue = nullptr)
{
    OGRFeature *poFeature =
        OGRFeature::CreateFeature(poOutLayer->GetLayerDefn());

    poFeature->SetField(FIELD_START, dfFrom);
    poFeature->SetField(FIELD_FINISH, dfTo);
    poFeature->SetField(FIELD_SCALE_FACTOR, dfScaleFactor);

    if (pszOutputSepFieldName != nullptr)
    {
        poFeature->SetField(pszOutputSepFieldName, pszOutputSepFieldValue);
    }

    poFeature->SetGeometryDirectly(pPart);

    if (poOutLayer->CreateFeature(poFeature) != OGRERR_NONE)
    {
        if (!bQuiet)
            printf("Failed to create feature in shapefile.\n");
        return OGRERR_FAILURE;
    }

    OGRFeature::DestroyFeature(poFeature);

    return OGRERR_NONE;
}

//------------------------------------------------------------------------
// CreateSubline
//------------------------------------------------------------------------
static OGRErr CreateSubline(OGRLayer *const poPkLayer, double dfPosBeg,
                            double dfPosEnd, OGRLayer *const poOutLayer,
                            CPL_UNUSED int bDisplayProgress, bool bQuiet)
{
    // Get step
    poPkLayer->ResetReading();
    OGRFeature *pFeature = poPkLayer->GetNextFeature();
    if (nullptr != pFeature)
    {
        // FIXME: Clang Static Analyzer rightly found that the following
        // code is dead
        // dfBeg = pFeature->GetFieldAsDouble(FIELD_START);
        // dfEnd = pFeature->GetFieldAsDouble(FIELD_FINISH);
        OGRFeature::DestroyFeature(pFeature);
    }
    else
    {
        fprintf(stderr, _("Get step for positions %f - %f failed\n"), dfPosBeg,
                dfPosEnd);
        return OGRERR_FAILURE;
    }
    // Get second part.
    double dfBeg = 0.0;
    double dfEnd = 0.0;
    pFeature = poPkLayer->GetNextFeature();
    if (nullptr != pFeature)
    {
        dfBeg = pFeature->GetFieldAsDouble(FIELD_START);
        dfEnd = pFeature->GetFieldAsDouble(FIELD_FINISH);
        OGRFeature::DestroyFeature(pFeature);
    }
    else
    {
        fprintf(stderr, _("Get step for positions %f - %f failed\n"), dfPosBeg,
                dfPosEnd);
        return OGRERR_FAILURE;
    }
    const double dfStep = dfEnd - dfBeg;

    // Round input to step
    const double dfPosBegLow = floor(dfPosBeg / dfStep) * dfStep;
    const double dfPosEndHigh = ceil(dfPosEnd / dfStep) * dfStep;

    CPLString szAttributeFilter;
    szAttributeFilter.Printf("%s >= %f AND %s <= %f", FIELD_START, dfPosBegLow,
                             FIELD_FINISH, dfPosEndHigh);
    // TODO: ExecuteSQL should be faster.
    poPkLayer->SetAttributeFilter(szAttributeFilter);
    poPkLayer->ResetReading();

    std::map<double, OGRFeature *> moParts;

    while ((pFeature = poPkLayer->GetNextFeature()) != nullptr)
    {
        double dfStart = pFeature->GetFieldAsDouble(FIELD_START);
        moParts[dfStart] = pFeature;
    }

    if (moParts.empty())
    {
        fprintf(stderr, _("Get parts for positions %f - %f failed\n"), dfPosBeg,
                dfPosEnd);
        return OGRERR_FAILURE;
    }

    OGRLineString SubLine;
    if (moParts.size() == 1)
    {
        std::map<double, OGRFeature *>::iterator IT = moParts.begin();
        const double dfStart = IT->first;
        double dfPosBegCorr = dfPosBeg - dfStart;
        const double dfSF = IT->second->GetFieldAsDouble(FIELD_SCALE_FACTOR);
        dfPosBegCorr *= dfSF;

        const double dfPosEndCorr = (dfPosEnd - dfStart) * dfSF;

        OGRLineString *pLine = IT->second->GetGeometryRef()->toLineString();

        OGRLineString *pSubLine =
            pLine->getSubLine(dfPosBegCorr, dfPosEndCorr, FALSE);

        OGRFeature::DestroyFeature(IT->second);
        // Store.
        return AddFeature(poOutLayer, pSubLine, dfPosBeg, dfPosEnd, 1.0,
                          bQuiet);
    }
    else
    {
        int nCounter = static_cast<int>(moParts.size());
        std::map<double, OGRFeature *>::iterator IT = moParts.begin();
        OGRLineString *pOutLine = new OGRLineString();
        // Get first part.
        const double dfStart = IT->first;
        double dfPosBegCorr = dfPosBeg - dfStart;
        double dfSF = IT->second->GetFieldAsDouble(FIELD_SCALE_FACTOR);
        dfPosBegCorr *= dfSF;

        OGRLineString *pLine = IT->second->GetGeometryRef()->toLineString();

        OGRLineString *pSubLine =
            pLine->getSubLine(dfPosBegCorr, pLine->get_Length(), FALSE);

        pOutLine->addSubLineString(pSubLine);
        delete pSubLine;
        OGRFeature::DestroyFeature(IT->second);

        ++IT;
        nCounter--;

        while (nCounter > 1)
        {
            pLine = IT->second->GetGeometryRef()->toLineString();
            pOutLine->addSubLineString(pLine);
            OGRFeature::DestroyFeature(IT->second);
            ++IT;
            nCounter--;
        }

        // Get last part
        double dfPosEndCorr = dfPosEnd - IT->first;
        dfSF = IT->second->GetFieldAsDouble(FIELD_SCALE_FACTOR);
        dfPosEndCorr *= dfSF;

        pLine = IT->second->GetGeometryRef()->toLineString();

        pSubLine = pLine->getSubLine(0, dfPosEndCorr, FALSE);

        pOutLine->addSubLineString(pSubLine);
        delete pSubLine;

        OGRFeature::DestroyFeature(IT->second);
        // Store
        return AddFeature(poOutLayer, pOutLine, dfPosBeg, dfPosEnd, 1.0,
                          bQuiet);
    }
}

//------------------------------------------------------------------------
// Project
//------------------------------------------------------------------------
#ifdef HAVE_GEOS
static double Project(OGRLineString *pLine, OGRPoint *pPoint)
{
    if (nullptr == pLine || nullptr == pPoint)
        return -1;
    OGRPoint TestPoint;
    pLine->StartPoint(&TestPoint);
    if (TestPoint.Equals(pPoint))
        return 0.0;
    pLine->EndPoint(&TestPoint);
    if (TestPoint.Equals(pPoint))
        return pLine->get_Length();

    return pLine->Project(pPoint);
}
#endif

//------------------------------------------------------------------------
// CreatePartsFromLineString
//------------------------------------------------------------------------
#ifdef HAVE_GEOS
static OGRErr CreatePartsFromLineString(
    OGRLineString *pPathGeom, OGRLayer *const poPkLayer, int nMValField,
    double dfStep, OGRLayer *const poOutLayer, int bDisplayProgress,
    bool bQuiet, const char *pszOutputSepFieldName = nullptr,
    const char *pszOutputSepFieldValue = nullptr)
{
    // Check repers/milestones/reference points type
    OGRwkbGeometryType eGeomType = poPkLayer->GetGeomType();
    if (wkbFlatten(eGeomType) != wkbPoint)
    {
        fprintf(stderr, _("Unsupported geometry type %s for path\n"),
                OGRGeometryTypeToName(eGeomType));
        return OGRERR_FAILURE;
    }

    double dfTolerance = 1.0;
    const OGRSpatialReference *pSpaRef = pPathGeom->getSpatialReference();
    if (pSpaRef->IsGeographic())
    {
        dfTolerance = TOLERANCE_DEGREE;
    }
    else
    {
        dfTolerance = TOLERANCE_METER;
    }

    // Create sorted list of repers.
    std::map<double, OGRPoint *> moRepers;
    poPkLayer->ResetReading();
    OGRFeature *pReperFeature = nullptr;
    while ((pReperFeature = poPkLayer->GetNextFeature()) != nullptr)
    {
        const double dfReperPos = pReperFeature->GetFieldAsDouble(nMValField);
        OGRGeometry *pGeom = pReperFeature->GetGeometryRef();
        if (nullptr != pGeom)
        {
            OGRPoint *pPt = pGeom->clone()->toPoint();
            if (!bQuiet)
            {
                if (moRepers.find(dfReperPos) != moRepers.end())
                {
                    CPLError(
                        CE_Warning, CPLE_AppDefined,
                        "The distance %f is already present in repers file!",
                        dfReperPos);
                }
            }
            // Check if reper is inside the path
            const double dfTestDistance = Project(pPathGeom, pPt);
            if (dfTestDistance < 0)
            {
                if (!bQuiet)
                {
                    CPLError(CE_Warning, CPLE_AppDefined,
                             "The distance %f is out of path!", dfReperPos);
                }
                delete pPt;
            }
            else
            {
                const double dfDist = pPathGeom->Distance(pPt);
                if (dfDist < dfTolerance)
                    moRepers[dfReperPos] = pPt;
                else
                    delete pPt;
            }
        }
        OGRFeature::DestroyFeature(pReperFeature);
    }

    if (moRepers.size() < 2)
    {
        fprintf(stderr, _("Not enough repers to proceed.\n"));
        return OGRERR_FAILURE;
    }

    // Check direction.
    if (!bQuiet)
    {
        fprintf(stdout, "Check path direction.\n");
    }

    // Get distance along path from pt1 and pt2.
    // If pt1 distance > pt2 distance, reverse path
    std::map<double, OGRPoint *>::const_iterator IT;
    IT = moRepers.begin();
    double dfPosition = IT->first;
    double dfBeginPosition = IT->first;
    OGRPoint *pt1 = IT->second;
    ++IT;
    OGRPoint *pt2 = IT->second;

    double dfDistance1 = Project(pPathGeom, pt1);
    double dfDistance2 = Project(pPathGeom, pt2);

    if (dfDistance1 > dfDistance2)
    {
        if (!bQuiet)
        {
            fprintf(stderr,
                    _("Warning: The path is opposite the repers direction. "
                      "Let's reverse path.\n"));
        }
        pPathGeom->reversePoints();

        dfDistance1 = Project(pPathGeom, pt1);
        dfDistance2 = Project(pPathGeom, pt2);
    }

    OGRLineString *pPart = nullptr;

    std::vector<CURVE_DATA> astSubLines;

    if (!bQuiet)
    {
        fprintf(stdout, "Create parts\n");
    }

    // Get first part
    // If first point is not at the beginning of the path
    // The first part should be from the beginning of the path
    // to the first point. length == part.getLength
    OGRPoint *pPtBeg = nullptr;
    OGRPoint *pPtEnd = nullptr;
    double dfPtBegPosition = 0.0;
    double dfPtEndPosition = 0.0;

    if (dfDistance1 > DELTA)
    {
        pPart = pPathGeom->getSubLine(0, dfDistance1, FALSE);
        if (nullptr != pPart)
        {
            double dfLen = pPart->get_Length();
            if (pSpaRef->IsGeographic())
            {
                // convert to UTM/WGS84
                OGRPoint pt;
                pPart->Value(dfLen / 2, &pt);
                const int nZoneEnv =
                    static_cast<int>(30 + (pt.getX() + 3.0) / 6.0 + 0.5);
                int nEPSG;
                if (pt.getY() > 0)
                {
                    nEPSG = 32600 + nZoneEnv;
                }
                else
                {
                    nEPSG = 32700 + nZoneEnv;
                }
                OGRSpatialReference SpatRef;
                SpatRef.importFromEPSG(nEPSG);
                OGRGeometry *pTransformPart = pPart->clone();
                if (pTransformPart->transformTo(&SpatRef) == OGRERR_NONE)
                {
                    OGRLineString *pTransformPartLS =
                        pTransformPart->toLineString();
                    dfLen = pTransformPartLS->get_Length();
                }

                CURVE_DATA data = {pPart, dfPosition - dfLen, dfPosition,
                                   pPart->get_Length() / dfLen};
                astSubLines.push_back(data);

                pPtBeg = new OGRPoint();
                pPart->getPoint(0, pPtBeg);
                dfPtBegPosition = dfPosition - dfLen;

                // AddFeature(poOutLayer, pPart, dfPosition - dfLen, dfPosition,
                //            pPart->get_Length() / dfLen, bQuiet);
                delete pTransformPart;
            }
            else
            {
                CURVE_DATA data = {pPart, dfPosition - dfLen, dfPosition, 1.0};
                astSubLines.push_back(data);
                // AddFeature(poOutLayer, pPart, dfPosition - dfLen, dfPosition,
                //            1.0, bQuiet);
                pPtBeg = new OGRPoint();
                pPart->getPoint(0, pPtBeg);
                dfPtBegPosition = dfPosition - dfLen;
            }
        }
    }

    if (dfDistance2 - dfDistance1 > DELTA)
    {
        pPart = pPathGeom->getSubLine(dfDistance1, dfDistance2, FALSE);
        if (nullptr != pPart)
        {
            CURVE_DATA data = {pPart, dfPosition, IT->first,
                               pPart->get_Length() / (IT->first - dfPosition)};
            astSubLines.push_back(data);
            // AddFeature(poOutLayer, pPart, dfPosition, IT->first,
            //            pPart->get_Length() / (IT->first - dfPosition),
            //            bQuiet);
        }
    }

    GDALProgressFunc pfnProgress = nullptr;
    void *pProgressArg = nullptr;

    double dfFactor = 1.0 / moRepers.size();
    if (bDisplayProgress)
    {
        pfnProgress = GDALScaledProgress;
        pProgressArg =
            GDALCreateScaledProgress(0.0, 1.0, GDALTermProgress, nullptr);
    }

    int nCount = 2;
    dfDistance1 = dfDistance2;
    dfPosition = IT->first;
    ++IT;  // Get third point

    double dfEndPosition = 0.0;
    while (IT != moRepers.end())
    {
        if (bDisplayProgress)
        {
            pfnProgress(nCount * dfFactor, "", pProgressArg);
            nCount++;
        }

        dfEndPosition = IT->first;

        dfDistance2 = Project(pPathGeom, IT->second);

        if (dfDistance2 - dfDistance1 > DELTA)
        {
            pPart = pPathGeom->getSubLine(dfDistance1, dfDistance2, FALSE);
            if (nullptr != pPart)
            {
                CURVE_DATA data = {pPart, dfPosition, IT->first,
                                   pPart->get_Length() /
                                       (IT->first - dfPosition)};
                astSubLines.push_back(data);
                // AddFeature(poOutLayer, pPart, dfPosition, IT->first,
                //            pPart->get_Length() / (IT->first - dfPosition),
                //            bQuiet);
                dfDistance1 = dfDistance2;
                dfPosition = IT->first;
            }
        }

        ++IT;
    }

    // Get last part
    if (pPathGeom->get_Length() - dfDistance1 > DELTA)
    {
        pPart =
            pPathGeom->getSubLine(dfDistance1, pPathGeom->get_Length(), FALSE);
        if (nullptr != pPart)
        {
            double dfLen = pPart->get_Length();
            if (pSpaRef->IsGeographic())
            {
                // convert to UTM/WGS84
                OGRPoint pt;
                pPart->Value(dfLen / 2, &pt);
                const int nZoneEnv =
                    static_cast<int>(30 + (pt.getX() + 3.0) / 6.0 + 0.5);
                int nEPSG;
                if (pt.getY() > 0)
                {
                    nEPSG = 32600 + nZoneEnv;
                }
                else
                {
                    nEPSG = 32700 + nZoneEnv;
                }
                OGRSpatialReference SpatRef;
                SpatRef.importFromEPSG(nEPSG);
                OGRGeometry *pTransformPart = pPart->clone();
                if (pTransformPart->transformTo(&SpatRef) == OGRERR_NONE)
                {
                    OGRLineString *pTransformPartLS =
                        pTransformPart->toLineString();
                    dfLen = pTransformPartLS->get_Length();
                }
                CURVE_DATA data = {pPart, dfPosition, dfPosition + dfLen,
                                   pPart->get_Length() / dfLen};
                astSubLines.push_back(data);
                // AddFeature(poOutLayer, pPart, dfPosition, dfPosition + dfLen,
                //            pPart->get_Length() / dfLen, bQuiet);

                pPtEnd = new OGRPoint();
                pPart->getPoint(pPart->getNumPoints() - 1, pPtEnd);
                dfPtEndPosition = dfPosition + dfLen;

                delete pTransformPart;
            }
            else
            {
                CURVE_DATA data = {pPart, dfPosition, dfPosition + dfLen, 1.0};
                astSubLines.push_back(data);
                // AddFeature(poOutLayer, pPart, dfPosition - dfLen, dfPosition,
                //            1.0, bQuiet);
                pPtEnd = new OGRPoint();
                pPart->getPoint(pPart->getNumPoints() - 1, pPtEnd);
                dfPtEndPosition = dfPosition + dfLen;
            }
        }
    }

    // Create pickets
    if (!bQuiet)
    {
        fprintf(stdout, "\nCreate pickets.\n");
    }

    const double dfRoundBeg = pPtBeg != nullptr
                                  ? ceil(dfPtBegPosition / dfStep) * dfStep
                                  : ceil(dfBeginPosition / dfStep) * dfStep;

    if (pPtEnd != nullptr)
        dfEndPosition = dfPtEndPosition;

    dfFactor = dfStep / (dfEndPosition - dfRoundBeg);
    nCount = 0;
    for (std::map<double, OGRPoint *>::iterator oIter = moRepers.begin();
         oIter != moRepers.end(); ++oIter)
    {
        delete oIter->second;
    }

    moRepers.clear();

    if (pPtBeg != nullptr)
        moRepers[dfPtBegPosition] = pPtBeg;
    if (pPtEnd != nullptr)
        moRepers[dfPtEndPosition] = pPtEnd;

    for (double dfDist = dfRoundBeg; dfDist <= dfEndPosition; dfDist += dfStep)
    {
        if (bDisplayProgress)
        {
            pfnProgress(nCount * dfFactor, "", pProgressArg);
            nCount++;
        }

        for (int j = 0; j < static_cast<int>(astSubLines.size()); j++)
        {
            if (astSubLines[j].IsInside(dfDist))
            {
                const double dfRealDist =
                    (dfDist - astSubLines[j].dfBeg) * astSubLines[j].dfFactor;
                OGRPoint *pReperPoint = new OGRPoint();
                astSubLines[j].pPart->Value(dfRealDist, pReperPoint);

                moRepers[dfDist] = pReperPoint;
                break;
            }
        }
    }

    for (int i = 0; i < static_cast<int>(astSubLines.size()); i++)
    {
        delete astSubLines[i].pPart;
    }
    astSubLines.clear();

    if (!bQuiet)
    {
        fprintf(stdout, "\nCreate sublines.\n");
    }

    IT = moRepers.begin();
    dfFactor = 1.0 / moRepers.size();
    nCount = 0;
    dfDistance1 = 0;
    dfPosition = IT->first;

    while (IT != moRepers.end())
    {
        if (bDisplayProgress)
        {
            pfnProgress(nCount * dfFactor, "", pProgressArg);
            nCount++;
        }

        dfDistance2 = Project(pPathGeom, IT->second);

        if (dfDistance2 - dfDistance1 > DELTA)
        {
            pPart = pPathGeom->getSubLine(dfDistance1, dfDistance2, FALSE);
            if (nullptr != pPart)
            {
                AddFeature(poOutLayer, pPart, dfPosition, IT->first,
                           pPart->get_Length() / (IT->first - dfPosition),
                           bQuiet, pszOutputSepFieldName,
                           pszOutputSepFieldValue);
                dfDistance1 = dfDistance2;
                dfPosition = IT->first;
            }
        }

        ++IT;
    }

    for (std::map<double, OGRPoint *>::iterator oIter = moRepers.begin();
         oIter != moRepers.end(); ++oIter)
    {
        delete oIter->second;
    }

    if (!bQuiet)
    {
        fprintf(stdout, "\nSuccess!\n\n");
    }

    if (nullptr != pProgressArg)
    {
        GDALDestroyScaledProgress(pProgressArg);
    }

    return OGRERR_NONE;
}
#endif

//------------------------------------------------------------------------
// CreateParts
//------------------------------------------------------------------------
#ifdef HAVE_GEOS
static OGRErr CreateParts(OGRLayer *const poLnLayer, OGRLayer *const poPkLayer,
                          int nMValField, double dfStep,
                          OGRLayer *const poOutLayer, int bDisplayProgress,
                          bool bQuiet,
                          const char *pszOutputSepFieldName = nullptr,
                          const char *pszOutputSepFieldValue = nullptr)
{
    OGRErr eRetCode = OGRERR_FAILURE;

    // check path and get first line
    OGRwkbGeometryType eGeomType = poLnLayer->GetGeomType();
    if (wkbFlatten(eGeomType) != wkbLineString &&
        wkbFlatten(eGeomType) != wkbMultiLineString)
    {
        fprintf(stderr, _("Unsupported geometry type %s for path.\n"),
                OGRGeometryTypeToName(eGeomType));
        return eRetCode;
    }

    poLnLayer->ResetReading();
    // Get first geometry
    // TODO: Attribute filter for path geometry.
    OGRFeature *pPathFeature = poLnLayer->GetNextFeature();
    if (nullptr != pPathFeature)
    {
        OGRGeometry *pGeom = pPathFeature->GetGeometryRef();

        if (pGeom != nullptr &&
            wkbFlatten(pGeom->getGeometryType()) == wkbMultiLineString)
        {
            if (!bQuiet)
            {
                fprintf(stdout,
                        _("\nThe geometry " CPL_FRMT_GIB
                          " is wkbMultiLineString type.\n"),
                        pPathFeature->GetFID());
            }

            OGRGeometryCollection *pGeomColl = pGeom->toGeometryCollection();
            for (int i = 0; i < pGeomColl->getNumGeometries(); ++i)
            {
                OGRLineString *pPath =
                    pGeomColl->getGeometryRef(i)->clone()->toLineString();
                pPath->assignSpatialReference(pGeomColl->getSpatialReference());
                eRetCode = CreatePartsFromLineString(
                    pPath, poPkLayer, nMValField, dfStep, poOutLayer,
                    bDisplayProgress, bQuiet, pszOutputSepFieldName,
                    pszOutputSepFieldValue);

                if (eRetCode != OGRERR_NONE)
                {
                    OGRFeature::DestroyFeature(pPathFeature);
                    return eRetCode;
                }
            }
        }
        else if (pGeom != nullptr &&
                 wkbFlatten(pGeom->getGeometryType()) == wkbLineString)
        {
            OGRLineString *pGeomClone = pGeom->clone()->toLineString();
            eRetCode = CreatePartsFromLineString(
                pGeomClone, poPkLayer, nMValField, dfStep, poOutLayer,
                bDisplayProgress, bQuiet, pszOutputSepFieldName,
                pszOutputSepFieldValue);
            delete pGeomClone;
        }

        OGRFeature::DestroyFeature(pPathFeature);
    }

    // Should never reach

    return eRetCode;
}
#endif

//------------------------------------------------------------------------
// CreatePartsMultiple
//------------------------------------------------------------------------
#ifdef HAVE_GEOS
static OGRErr CreatePartsMultiple(
    OGRLayer *const poLnLayer, const char *pszLineSepFieldName,
    OGRLayer *const poPkLayer, const char *pszPicketsSepFieldName,
    int nMValField, double dfStep, OGRLayer *const poOutLayer,
    const char *pszOutputSepFieldName, int bDisplayProgress, bool bQuiet)
{
    // Read all separate field values into array
    OGRFeatureDefn *pDefn = poLnLayer->GetLayerDefn();
    const int nLineSepFieldInd = pDefn->GetFieldIndex(pszLineSepFieldName);
    if (nLineSepFieldInd == -1)
    {
        fprintf(stderr, _("The field was %s not found.\n"),
                pszLineSepFieldName);
        return OGRERR_FAILURE;
    }

    poLnLayer->ResetReading();

    std::set<CPLString> asIDs;
    OGRFeature *pFeature = nullptr;
    while ((pFeature = poLnLayer->GetNextFeature()) != nullptr)
    {
        CPLString sID = pFeature->GetFieldAsString(nLineSepFieldInd);
        asIDs.insert(sID);

        OGRFeature::DestroyFeature(pFeature);
    }

    for (std::set<CPLString>::const_iterator it = asIDs.begin();
         it != asIDs.end(); ++it)
    {
        // Create select clause
        CPLString sLineWhere;
        sLineWhere.Printf("%s = '%s'", pszLineSepFieldName, it->c_str());
        poLnLayer->SetAttributeFilter(sLineWhere);

        CPLString sPkWhere;
        sPkWhere.Printf("%s = '%s'", pszPicketsSepFieldName, it->c_str());
        poPkLayer->SetAttributeFilter(sPkWhere);

        if (!bQuiet)
        {
            fprintf(stdout, "The %s %s\n", pszPicketsSepFieldName, it->c_str());
        }

        // Don't check success as we want to try all paths
        CreateParts(poLnLayer, poPkLayer, nMValField, dfStep, poOutLayer,
                    bDisplayProgress, bQuiet, pszOutputSepFieldName, *it);
    }

    return OGRERR_NONE;
}
#endif

//------------------------------------------------------------------------
// GetPosition
//------------------------------------------------------------------------
#ifdef HAVE_GEOS
static OGRErr GetPosition(OGRLayer *const poPkLayer, double dfX, double dfY,
                          int /* bDisplayProgress */, int bQuiet)
{
    // Create point
    OGRPoint pt;
    pt.setX(dfX);
    pt.setY(dfY);
    pt.assignSpatialReference(poPkLayer->GetSpatialRef());

    poPkLayer->ResetReading();
    OGRLineString *pCloserPart = nullptr;
    double dfBeg = 0.0;
    double dfScale = 0.0;
    double dfMinDistance = std::numeric_limits<double>::max();
    OGRFeature *pFeature = nullptr;
    while ((pFeature = poPkLayer->GetNextFeature()) != nullptr)
    {
        OGRGeometry *pCurrentGeom = pFeature->GetGeometryRef();
        if (pCurrentGeom != nullptr)
        {
            double dfCurrentDistance = pCurrentGeom->Distance(&pt);
            if (dfCurrentDistance < dfMinDistance)
            {
                dfMinDistance = dfCurrentDistance;
                if (pCloserPart != nullptr)
                    delete pCloserPart;
                pCloserPart = pFeature->StealGeometry()->toLineString();
                dfBeg = pFeature->GetFieldAsDouble(FIELD_START);
                dfScale = pFeature->GetFieldAsDouble(FIELD_SCALE_FACTOR);
            }
        }
        OGRFeature::DestroyFeature(pFeature);
    }

    if (nullptr == pCloserPart)
    {
        fprintf(stderr, _("Failed to find closest part.\n"));
        return OGRERR_FAILURE;
    }
    // Now we have closest part
    // Get real distance
    const double dfRealDist = Project(pCloserPart, &pt);
    delete pCloserPart;
    // Compute reference distance
    const double dfRefDist = dfBeg + dfRealDist / dfScale;
    if (bQuiet)
    {
        fprintf(stdout, "%s", CPLSPrintf("%f\n", dfRefDist));
    }
    else
    {
        fprintf(stdout, "%s",
                CPLSPrintf(
                    _("The position for coordinates lat:%f, long:%f is %f\n"),
                    dfY, dfX, dfRefDist));
    }

    return OGRERR_NONE;
}
#endif

//------------------------------------------------------------------------
// GetCoordinates
//------------------------------------------------------------------------
static OGRErr GetCoordinates(OGRLayer *const poPkLayer, double dfPos,
                             /* CPL_UNUSED */ int /* bDisplayProgress */,
                             bool bQuiet)
{
    CPLString szAttributeFilter;
    szAttributeFilter.Printf("%s < %f AND %s > %f", FIELD_START, dfPos,
                             FIELD_FINISH, dfPos);
    // TODO: ExecuteSQL should be faster.
    poPkLayer->SetAttributeFilter(szAttributeFilter);
    poPkLayer->ResetReading();

    bool bHaveCoords = false;
    for (auto &pFeature : poPkLayer)
    {
        bHaveCoords = true;
        const double dfStart = pFeature->GetFieldAsDouble(FIELD_START);
        double dfPosCorr = dfPos - dfStart;
        const double dfSF = pFeature->GetFieldAsDouble(FIELD_SCALE_FACTOR);
        dfPosCorr *= dfSF;
        OGRLineString *pLine = pFeature->GetGeometryRef()->toLineString();

        OGRPoint pt;
        pLine->Value(dfPosCorr, &pt);

        if (bQuiet)
        {
            fprintf(stdout, "%s",
                    CPLSPrintf("%f,%f,%f\n", pt.getX(), pt.getY(), pt.getZ()));
        }
        else
        {
            fprintf(stdout, "%s",
                    CPLSPrintf(_("The position for distance %f is lat:%f, "
                                 "long:%f, height:%f\n"),
                               dfPos, pt.getY(), pt.getX(), pt.getZ()));
        }
    }

    if (bHaveCoords)
    {
        return OGRERR_NONE;
    }
    else
    {
        fprintf(stderr, _("Get coordinates for position %f failed.\n"), dfPos);
        return OGRERR_FAILURE;
    }
}

/************************************************************************/
/*                           OGRLineRefOptions                          */
/************************************************************************/

struct OGRLineRefOptions
{
    bool bQuiet = false;
    bool bDisplayProgress = false;
    std::string osFormat = "ESRI Shapefile";

    std::string osSrcLineDataSourceName;
    std::string osSrcLineLayerName;
#ifdef HAVE_GEOS
    std::string osSrcLineSepFieldName;
#endif

    std::string osSrcPicketsDataSourceName;
#ifdef HAVE_GEOS
    std::string osSrcPicketsLayerName;
    std::string osSrcPicketsSepFieldName;
    std::string osSrcPicketsMFieldName;
#endif

    std::string osSrcPartsDataSourceName;
    std::string osSrcPartsLayerName;

#ifdef HAVE_GEOS
    std::string osOutputSepFieldName = "uniq_uid";
#endif
    std::string osOutputDataSourceName;
    std::string osOutputLayerName;

    CPLStringList aosDSCO;
    CPLStringList aosLCO;

    // Operations
    bool bCreate = false;
    bool bGetPos = false;
    bool bGetSubLine = false;
    bool bGetCoord = false;

#ifdef HAVE_GEOS
    double dfXPos = std::numeric_limits<double>::quiet_NaN();
    double dfYPos = std::numeric_limits<double>::quiet_NaN();
    double dfStep = std::numeric_limits<double>::quiet_NaN();
#endif
    double dfPosBeg = std::numeric_limits<double>::quiet_NaN();
    double dfPosEnd = std::numeric_limits<double>::quiet_NaN();
    double dfPos = std::numeric_limits<double>::quiet_NaN();
};

/************************************************************************/
/*                           OGRLineRefGetParser                        */
/************************************************************************/

static std::unique_ptr<GDALArgumentParser>
OGRLineRefAppOptionsGetParser(OGRLineRefOptions *psOptions)
{
    auto argParser = std::make_unique<GDALArgumentParser>(
        "ogrlineref", /* bForBinary */ true);

    argParser->add_description(
        _("Create linear reference and provide some calculations using it."));

    argParser->add_epilog(_("For more details, consult the full documentation "
                            "for the ogrlineref utility "
                            "https://gdal.org/programs/ogrlineref.html"));

    auto &quietArg{argParser->add_quiet_argument(&psOptions->bQuiet)};
    argParser->add_hidden_alias_for(quietArg, "-quiet");

    argParser->add_argument("-progress")
        .flag()
        .store_into(psOptions->bDisplayProgress)
        .help(_("Display progress."));

    argParser->add_output_format_argument(psOptions->osFormat);

    argParser->add_dataset_creation_options_argument(psOptions->aosDSCO);

    argParser->add_layer_creation_options_argument(psOptions->aosLCO);

#ifdef HAVE_GEOS
    argParser->add_argument("-create")
        .flag()
        .store_into(psOptions->bCreate)
        .help(_("Create the linear reference file (linestring of parts)."));
#endif

    argParser->add_argument("-l")
        .metavar("<src_line_datasource_name>")
        .store_into(psOptions->osSrcLineDataSourceName)
        .help(_("Name of the line path datasource."));

    argParser->add_argument("-ln")
        .metavar("<layer_name>")
        .store_into(psOptions->osSrcLineLayerName)
        .help(_("Layer name in the line path datasource."));

#ifdef HAVE_GEOS

    argParser->add_argument("-lf")
        .metavar("<field_name>")
        .store_into(psOptions->osSrcLineSepFieldName)
        .help(_("Field name for unique paths in layer."));
#endif

    argParser->add_argument("-p")
        .metavar("<src_repers_datasource_name>")
        .store_into(psOptions->osSrcPicketsDataSourceName)
        .help(_("Datasource of repers name."));

#ifdef HAVE_GEOS
    argParser->add_argument("-pn")
        .metavar("<layer_name>")
        .store_into(psOptions->osSrcPicketsLayerName)
        .help(_("Layer name in repers datasource."));

    argParser->add_argument("-pm")
        .metavar("<pos_field_name>")
        .store_into(psOptions->osSrcPicketsMFieldName)
        .help(_("Line position field name."));

    argParser->add_argument("-pf")
        .metavar("<field_name>")
        .store_into(psOptions->osSrcPicketsSepFieldName)
        .help(_("Field name of unique values to map input reference points "
                "to lines."));
#endif

    argParser->add_argument("-r")
        .metavar("<src_parts_datasource_name>")
        .store_into(psOptions->osSrcPartsDataSourceName)
        .help(_("Path to linear reference file."));

    argParser->add_argument("-rn")
        .metavar("<layer_name>")
        .store_into(psOptions->osSrcPartsLayerName)
        .help(_("Name of the layer in the input linear reference datasource."));

    argParser->add_argument("-o")
        .metavar("<dst_datasource_name>")
        .store_into(psOptions->osOutputDataSourceName)
        .help(_("Path to output linear reference file (linestring "
                "datasource)."));

    argParser->add_argument("-on")
        .metavar("<layer_name>")
        .store_into(psOptions->osOutputLayerName)
        .help(_("Name of the layer in the output linear reference "
                "datasource."));

#ifdef HAVE_GEOS
    argParser->add_argument("-of")
        .metavar("<field_name>")
        .store_into(psOptions->osOutputSepFieldName)
        .help(_(
            "Name of the field for storing the unique values of input lines."));

    argParser->add_argument("-s")
        .metavar("<step>")
        .scan<'g', double>()
        .store_into(psOptions->dfStep)
        .help(_("Part size in linear units."));

    argParser->add_argument("-get_pos")
        .flag()
        .store_into(psOptions->bGetPos)
        .help(_("Get the position for the given coordinates."));

    argParser->add_argument("-x")
        .metavar("<x>")
        .scan<'g', double>()
        .store_into(psOptions->dfXPos)
        .help(_("X coordinate."));

    argParser->add_argument("-y")
        .metavar("<y>")
        .scan<'g', double>()
        .store_into(psOptions->dfYPos)
        .help(_("Y coordinate."));
#endif

    argParser->add_argument("-get_coord")
        .flag()
        .store_into(psOptions->bGetCoord)
        .help(_("Return point on path for input linear distance."));

    argParser->add_argument("-m")
        .metavar("<position>")
        .scan<'g', double>()
        .store_into(psOptions->dfPos)
        .help(_("Input linear distance."));

    argParser->add_argument("-get_subline")
        .flag()
        .store_into(psOptions->bGetSubLine)
        .help(_("Return the portion of the input path from and to input linear "
                "positions."));

    argParser->add_argument("-mb")
        .metavar("<position>")
        .scan<'g', double>()
        .store_into(psOptions->dfPosBeg)
        .help(_("Input linear distance begin."));

    argParser->add_argument("-me")
        .metavar("<position>")
        .scan<'g', double>()
        .store_into(psOptions->dfPosEnd)
        .help(_("Input linear distance end."));

    return argParser;
}

/************************************************************************/
/*                                main()                                */
/************************************************************************/

MAIN_START(argc, argv)

{
    OGRErr eErr = OGRERR_NONE;

    operation stOper = op_unknown;

    EarlySetConfigOptions(argc, argv);

    argc = GDALGeneralCmdLineProcessor(argc, &argv, 0);

    if (argc < 2)
    {
        try
        {
            OGRLineRefOptions sOptions;
            auto argParser = OGRLineRefAppOptionsGetParser(&sOptions);
            fprintf(stderr, "%s\n", argParser->usage().c_str());
        }
        catch (const std::exception &err)
        {
            CPLError(CE_Failure, CPLE_AppDefined, "Unexpected exception: %s",
                     err.what());
        }
        CSLDestroy(argv);
        exit(1);
    }

    OGRRegisterAll();

    OGRLineRefOptions psOptions;
    auto argParser = OGRLineRefAppOptionsGetParser(&psOptions);

    try
    {
        argParser->parse_args_without_binary_name(argv + 1);
        CSLDestroy(argv);
    }
    catch (const std::exception &error)
    {
        argParser->display_error_and_usage(error);
        CSLDestroy(argv);
        exit(1);
    }

    // Select operation mode
    if (psOptions.bCreate)
        stOper = op_create;

    if (psOptions.bGetPos)
    {
        if (stOper != op_unknown)
        {
            fprintf(stderr, _("Only one operation can be specified\n"));
            argParser->usage();
            exit(1);
        }
        stOper = op_get_pos;
    }

    if (psOptions.bGetCoord)
    {
        if (stOper != op_unknown)
        {
            fprintf(stderr, _("Only one operation can be specified\n"));
            argParser->usage();
            exit(1);
        }
        stOper = op_get_coord;
    }

    if (psOptions.bGetSubLine)
    {
        if (stOper != op_unknown)
        {
            fprintf(stderr, _("Only one operation can be specified\n"));
            argParser->usage();
            exit(1);
        }
        stOper = op_get_subline;
    }

    if (stOper == op_unknown)
    {
        fprintf(stderr, _("No operation specified\n"));
        argParser->usage();
        exit(1);
    }

    /* -------------------------------------------------------------------- */
    /*                     Create linear reference                          */
    /* -------------------------------------------------------------------- */

    switch (stOper)
    {
        case op_create:
        {
#ifdef HAVE_GEOS
            if (psOptions.osOutputDataSourceName.empty())
            {
                fprintf(stderr, _("No output datasource provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.osSrcLineDataSourceName.empty())
            {
                fprintf(stderr, _("No path datasource provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.osSrcPicketsMFieldName.empty())
            {
                fprintf(stderr, _("No repers position field provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.osSrcPicketsDataSourceName.empty())
            {
                fprintf(stderr, _("No repers datasource provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.dfStep == std::numeric_limits<double>::quiet_NaN())
            {
                fprintf(stderr, _("No step provided.\n"));
                argParser->usage();
                exit(1);
            }

            /* ------------------------------------------------------------- */
            /*      Open data source.                                        */
            /* ------------------------------------------------------------- */

            GDALDataset *poLnDS = GDALDataset::FromHandle(OGROpen(
                psOptions.osSrcLineDataSourceName.c_str(), FALSE, nullptr));

            /* ------------------------------------------------------------- */
            /*      Report failure                                           */
            /* ------------------------------------------------------------- */
            if (poLnDS == nullptr)
            {
                OGRSFDriverRegistrar *poR =
                    OGRSFDriverRegistrar::GetRegistrar();

                fprintf(stderr,
                        _("FAILURE:\n"
                          "Unable to open path datasource `%s' with "
                          "the following drivers.\n"),
                        psOptions.osSrcLineDataSourceName.c_str());

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> %s\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }

                exit(1);
            }

            GDALDataset *poPkDS = GDALDataset::FromHandle(OGROpen(
                psOptions.osSrcPicketsDataSourceName.c_str(), FALSE, nullptr));

            /* --------------------------------------------------------------- */
            /*      Report failure                                             */
            /* --------------------------------------------------------------- */

            if (poPkDS == nullptr)
            {
                OGRSFDriverRegistrar *poR =
                    OGRSFDriverRegistrar::GetRegistrar();

                fprintf(stderr,
                        _("FAILURE:\n"
                          "Unable to open repers datasource `%s' "
                          "with the following drivers.\n"),
                        psOptions.osSrcPicketsDataSourceName.c_str());

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> %s\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }

                exit(1);
            }

            /* ----------------------------------------------------------------- */
            /*      Find the output driver.                                      */
            /* ----------------------------------------------------------------- */

            if (!psOptions.bQuiet)
                CheckDestDataSourceNameConsistency(
                    psOptions.osOutputDataSourceName.c_str(),
                    psOptions.osFormat.c_str());

            OGRSFDriverRegistrar *poR = OGRSFDriverRegistrar::GetRegistrar();

            GDALDriver *poDriver =
                poR->GetDriverByName(psOptions.osFormat.c_str());
            if (poDriver == nullptr)
            {
                fprintf(stderr, _("Unable to find driver `%s'.\n"),
                        psOptions.osFormat.c_str());
                fprintf(stderr, _("The following drivers are available:\n"));

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> `%s'\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }
                exit(1);
            }

            if (!CPLTestBool(CSLFetchNameValueDef(poDriver->GetMetadata(),
                                                  GDAL_DCAP_CREATE, "FALSE")))
            {
                fprintf(stderr,
                        _("%s driver does not support data source creation.\n"),
                        psOptions.osSrcPicketsDataSourceName.c_str());
                exit(1);
            }

            /* ---------------------------------------------------------------- */
            /*      Create the output data source.                              */
            /* ---------------------------------------------------------------- */
            GDALDataset *poODS =
                poDriver->Create(psOptions.osOutputDataSourceName.c_str(), 0, 0,
                                 0, GDT_Unknown, psOptions.aosDSCO);
            if (poODS == nullptr)
            {
                fprintf(stderr, _("%s driver failed to create %s.\n"),
                        psOptions.osFormat.c_str(),
                        psOptions.osOutputDataSourceName.c_str());
                exit(1);
            }

            OGRLayer *poLnLayer =
                psOptions.osSrcLineLayerName.empty()
                    ? poLnDS->GetLayer(0)
                    : poLnDS->GetLayerByName(
                          psOptions.osSrcLineLayerName.c_str());

            if (poLnLayer == nullptr)
            {
                fprintf(stderr, _("Get path layer failed.\n"));
                exit(1);
            }

            OGRLayer *poPkLayer =
                psOptions.osSrcPicketsLayerName.empty()
                    ? poPkDS->GetLayer(0)
                    : poPkDS->GetLayerByName(
                          psOptions.osSrcPicketsLayerName.c_str());

            if (poPkLayer == nullptr)
            {
                fprintf(stderr, _("Get repers layer failed.\n"));
                exit(1);
            }

            OGRFeatureDefn *poPkFDefn = poPkLayer->GetLayerDefn();
            int nMValField = poPkFDefn->GetFieldIndex(
                psOptions.osSrcPicketsMFieldName.c_str());

            OGRLayer *poOutLayer = nullptr;
            if (!psOptions.osSrcLineSepFieldName.empty() &&
                !psOptions.osSrcPicketsSepFieldName.empty())
            {
                poOutLayer =
                    SetupTargetLayer(poLnLayer, poODS, psOptions.aosLCO,
                                     psOptions.osOutputLayerName.c_str(),
                                     psOptions.osOutputSepFieldName.c_str());
                if (poOutLayer == nullptr)
                {
                    fprintf(stderr, _("Create output layer failed.\n"));
                    exit(1);
                }

                // Do the work
                eErr = CreatePartsMultiple(
                    poLnLayer, psOptions.osSrcLineSepFieldName.c_str(),
                    poPkLayer, psOptions.osSrcPicketsSepFieldName.c_str(),
                    nMValField, psOptions.dfStep, poOutLayer,
                    psOptions.osOutputSepFieldName.c_str(),
                    psOptions.bDisplayProgress, psOptions.bQuiet);
            }
            else
            {
                poOutLayer =
                    SetupTargetLayer(poLnLayer, poODS, psOptions.aosLCO,
                                     psOptions.osOutputLayerName.c_str());
                if (poOutLayer == nullptr)
                {
                    fprintf(stderr, _("Create output layer failed.\n"));
                    exit(1);
                }

                // Do the work
                eErr = CreateParts(
                    poLnLayer, poPkLayer, nMValField, psOptions.dfStep,
                    poOutLayer, psOptions.bDisplayProgress, psOptions.bQuiet);
            }

            GDALClose(poLnDS);
            GDALClose(poPkDS);
            if (GDALClose(poODS) != CE_None)
                eErr = CE_Failure;

#else   // HAVE_GEOS
            fprintf(stderr,
                    _("GEOS support not enabled or incompatible version.\n"));
            exit(1);
#endif  // HAVE_GEOS
            break;
        }
        case op_get_pos:
        {
#ifdef HAVE_GEOS

            if (psOptions.dfXPos == std::numeric_limits<double>::quiet_NaN() ||
                psOptions.dfYPos == std::numeric_limits<double>::quiet_NaN())
            {
                fprintf(stderr, _("no coordinates provided\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.osSrcPartsDataSourceName.empty())
            {
                fprintf(stderr, _("no parts datasource provided\n"));
                argParser->usage();
                exit(1);
            }

            GDALDataset *poPartsDS = GDALDataset::FromHandle(OGROpen(
                psOptions.osSrcPartsDataSourceName.c_str(), FALSE, nullptr));
            /* ------------------------------------------------------------------ */
            /*      Report failure                                                */
            /* ------------------------------------------------------------------ */
            if (poPartsDS == nullptr)
            {
                OGRSFDriverRegistrar *poR =
                    OGRSFDriverRegistrar::GetRegistrar();

                fprintf(stderr,
                        _("FAILURE:\n"
                          "Unable to open parts datasource `%s' with "
                          "the following drivers.\n"),
                        psOptions.osSrcPicketsDataSourceName.c_str());

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> %s\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }

                exit(1);
            }

            OGRLayer *poPartsLayer =
                psOptions.osSrcPartsLayerName.empty()
                    ? poPartsDS->GetLayer(0)
                    : poPartsDS->GetLayerByName(
                          psOptions.osSrcPartsLayerName.c_str());

            if (poPartsLayer == nullptr)
            {
                fprintf(stderr, _("Get parts layer failed.\n"));
                exit(1);
            }

            // Do the work
            eErr = GetPosition(poPartsLayer, psOptions.dfXPos, psOptions.dfYPos,
                               psOptions.bDisplayProgress, psOptions.bQuiet);

            GDALClose(poPartsDS);

#else   // HAVE_GEOS
            fprintf(stderr,
                    "GEOS support not enabled or incompatible version.\n");
            exit(1);
#endif  // HAVE_GEOS
            break;
        }
        case op_get_coord:
        {
            if (psOptions.osSrcPartsDataSourceName.empty())
            {
                fprintf(stderr, _("No parts datasource provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.dfPos == std::numeric_limits<double>::quiet_NaN())
            {
                fprintf(stderr, _("No position provided.\n"));
                argParser->usage();
                exit(1);
            }

            GDALDataset *poPartsDS = GDALDataset::FromHandle(OGROpen(
                psOptions.osSrcPartsDataSourceName.c_str(), FALSE, nullptr));
            /* ----------------------------------------------------------------- */
            /*      Report failure                                               */
            /* ----------------------------------------------------------------- */
            if (poPartsDS == nullptr)
            {
                OGRSFDriverRegistrar *poR =
                    OGRSFDriverRegistrar::GetRegistrar();

                fprintf(stderr,
                        _("FAILURE:\n"
                          "Unable to open parts datasource `%s' with "
                          "the following drivers.\n"),
                        psOptions.osSrcPicketsDataSourceName.c_str());

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> %s\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }

                exit(1);
            }

            OGRLayer *poPartsLayer =
                psOptions.osSrcPartsLayerName.empty()
                    ? poPartsDS->GetLayer(0)
                    : poPartsDS->GetLayerByName(
                          psOptions.osSrcPartsLayerName.c_str());

            if (poPartsLayer == nullptr)
            {
                fprintf(stderr, _("Get parts layer failed.\n"));
                exit(1);
            }
            // Do the work
            eErr = GetCoordinates(poPartsLayer, psOptions.dfPos,
                                  psOptions.bDisplayProgress, psOptions.bQuiet);

            GDALClose(poPartsDS);

            break;
        }
        case op_get_subline:
        {
            if (psOptions.dfPosBeg == std::numeric_limits<double>::quiet_NaN())
            {
                fprintf(stderr, _("No begin position provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.dfPosEnd == std::numeric_limits<double>::quiet_NaN())
            {
                fprintf(stderr, _("No end position provided.\n"));
                argParser->usage();
                exit(1);
            }
            if (psOptions.osSrcPartsDataSourceName.empty())
            {
                fprintf(stderr, _("No parts datasource provided.\n"));
                argParser->usage();
                exit(1);
            }

            GDALDataset *poPartsDS = GDALDataset::FromHandle(OGROpen(
                psOptions.osSrcPartsDataSourceName.c_str(), FALSE, nullptr));

            // Report failure.
            if (poPartsDS == nullptr)
            {
                OGRSFDriverRegistrar *poR =
                    OGRSFDriverRegistrar::GetRegistrar();

                fprintf(stderr,
                        _("FAILURE:\n"
                          "Unable to open parts datasource `%s' with "
                          "the following drivers.\n"),
                        psOptions.osSrcPicketsDataSourceName.c_str());

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> %s\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }

                exit(1);
            }

            // Find the output driver.
            if (!psOptions.bQuiet)
                CheckDestDataSourceNameConsistency(
                    psOptions.osOutputDataSourceName.c_str(),
                    psOptions.osFormat.c_str());

            OGRSFDriverRegistrar *poR = OGRSFDriverRegistrar::GetRegistrar();

            GDALDriver *poDriver =
                poR->GetDriverByName(psOptions.osFormat.c_str());
            if (poDriver == nullptr)
            {
                fprintf(stderr, _("Unable to find driver `%s'.\n"),
                        psOptions.osFormat.c_str());
                fprintf(stderr, _("The following drivers are available:\n"));

                for (int iDriver = 0; iDriver < poR->GetDriverCount();
                     iDriver++)
                {
                    fprintf(stderr, "  -> `%s'\n",
                            poR->GetDriver(iDriver)->GetDescription());
                }
                exit(1);
            }

            if (!CPLTestBool(CSLFetchNameValueDef(poDriver->GetMetadata(),
                                                  GDAL_DCAP_CREATE, "FALSE")))
            {
                fprintf(stderr,
                        _("%s driver does not support data source creation.\n"),
                        psOptions.osFormat.c_str());
                exit(1);
            }

            // Create the output data source.

            GDALDataset *poODS =
                poDriver->Create(psOptions.osOutputDataSourceName.c_str(), 0, 0,
                                 0, GDT_Unknown, psOptions.aosDSCO);
            if (poODS == nullptr)
            {
                fprintf(stderr, _("%s driver failed to create %s\n"),
                        psOptions.osFormat.c_str(),
                        psOptions.osOutputDataSourceName.c_str());
                exit(1);
            }

            OGRLayer *poPartsLayer =
                psOptions.osSrcLineLayerName.empty()
                    ? poPartsDS->GetLayer(0)
                    : poPartsDS->GetLayerByName(
                          psOptions.osSrcLineLayerName.c_str());

            if (poPartsLayer == nullptr)
            {
                fprintf(stderr, _("Get parts layer failed.\n"));
                exit(1);
            }

            OGRLayer *poOutLayer =
                SetupTargetLayer(poPartsLayer, poODS, psOptions.aosLCO,
                                 psOptions.osOutputLayerName.c_str());

            if (poOutLayer == nullptr)
            {
                fprintf(stderr, _("Create output layer failed.\n"));
                exit(1);
            }

            // Do the work

            eErr = CreateSubline(poPartsLayer, psOptions.dfPosBeg,
                                 psOptions.dfPosEnd, poOutLayer,
                                 psOptions.bDisplayProgress, psOptions.bQuiet);

            GDALClose(poPartsDS);
            if (GDALClose(poODS) != CE_None)
                eErr = CE_Failure;

            break;
        }
        default:
            fprintf(stderr, _("Unknown operation.\n"));
            argParser->usage();
            exit(1);
    }

    OGRCleanupAll();

#ifdef DBMALLOC
    malloc_dump(1);
#endif

    return eErr == OGRERR_NONE ? 0 : 1;
}

MAIN_END
