/******************************************************************************
 *
 * Project:  NTF Translator
 * Purpose:  NTFFileReader methods related to establishing the schemas
 *           of features that could occur in this product and the functions
 *           for actually performing the NTFRecord to OGRFeature conversion.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 1999, Frank Warmerdam
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include <stdarg.h>
#include "ntf.h"
#include "cpl_string.h"

/************************************************************************/
/*                         TranslateCodePoint()                         */
/*                                                                      */
/*      Used for code point, and code point plus.                       */
/************************************************************************/

static OGRFeature *TranslateCodePoint(NTFFileReader *poReader,
                                      OGRNTFLayer *poLayer,
                                      NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    // Attributes
    if (EQUAL(poLayer->GetLayerDefn()->GetName(), "CODE_POINT"))
        poReader->ApplyAttributeValues(
            poFeature, papoGroup, "PC", 1, "PQ", 2, "PR", 3, "TP", 4, "DQ", 5,
            "RP", 6, "BP", 7, "PD", 8, "MP", 9, "UM", 10, "RV", 11, NULL);
    else
        poReader->ApplyAttributeValues(
            poFeature, papoGroup, "PC", 1, "PQ", 2, "PR", 3, "TP", 4, "DQ", 5,
            "RP", 6, "BP", 7, "PD", 8, "MP", 9, "UM", 10, "RV", 11, "RH", 12,
            "LH", 13, "CC", 14, "DC", 15, "WC", 16, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateAddressPoint()                        */
/************************************************************************/

static OGRFeature *TranslateAddressPoint(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // CHG_TYPE
    poFeature->SetField(17, papoGroup[0]->GetField(22, 22));

    // CHG_DATE
    poFeature->SetField(18, papoGroup[0]->GetField(23, 28));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    // Attributes
    poReader->ApplyAttributeValues(
        poFeature, papoGroup, "OA", 1, "ON", 2, "DP", 3, "PB", 4, "SB", 5, "BD",
        6, "BN", 7, "DR", 8, "TN", 9, "DD", 10, "DL", 11, "PT", 12, "CN", 13,
        "PC", 14, "SF", 15, "RV", 16, NULL);

    return poFeature;
}

/************************************************************************/
/*                        TranslateOscarPoint()                         */
/*                                                                      */
/*      Used for OSCAR Traffic and Asset datasets.                      */
/************************************************************************/

static OGRFeature *TranslateOscarPoint(NTFFileReader *poReader,
                                       OGRNTFLayer *poLayer,
                                       NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "OD", 3, "JN",
                                   4, "SN", 5, NULL);

    return poFeature;
}

/************************************************************************/
/*                         TranslateOscarLine()                         */
/************************************************************************/

static OGRFeature *TranslateOscarLine(NTFFileReader *poReader,
                                      OGRNTFLayer *poLayer,
                                      NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "OD", 3, "PN",
                                   4, "LL", 5, "SC", 6, "FW", 7, "RN", 8, "TR",
                                   9, NULL);

    return poFeature;
}

/************************************************************************/
/*                      TranslateOscarRoutePoint()                      */
/************************************************************************/

static OGRFeature *TranslateOscarRoutePoint(NTFFileReader *poReader,
                                            OGRNTFLayer *poLayer,
                                            NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "OD", 3, "JN",
                                   4, "SN", 5, "NP", 6, "RT", 8, NULL);

    // PARENT_OSODR
    char **papszTypes, **papszValues;

    if (poReader->ProcessAttRecGroup(papoGroup, &papszTypes, &papszValues))
    {
        char **papszOSODRList = nullptr;

        for (int i = 0; papszTypes != nullptr && papszTypes[i] != nullptr; i++)
        {
            if (EQUAL(papszTypes[i], "PO"))
                papszOSODRList = CSLAddString(papszOSODRList, papszValues[i]);
        }

        poFeature->SetField(7, papszOSODRList);
        CPLAssert(CSLCount(papszOSODRList) == poFeature->GetFieldAsInteger(6));

        CSLDestroy(papszOSODRList);
        CSLDestroy(papszTypes);
        CSLDestroy(papszValues);
    }

    return poFeature;
}

/************************************************************************/
/*                      TranslateOscarRouteLine()                       */
/************************************************************************/

static OGRFeature *TranslateOscarRouteLine(NTFFileReader *poReader,
                                           OGRNTFLayer *poLayer,
                                           NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "OD", 3, "PN",
                                   4, "LL", 5, "RN", 6, "TR", 7, "NP", 8, NULL);

    // PARENT_OSODR
    char **papszTypes, **papszValues;

    if (poReader->ProcessAttRecGroup(papoGroup, &papszTypes, &papszValues))
    {
        char **papszOSODRList = nullptr;

        for (int i = 0; papszTypes != nullptr && papszTypes[i] != nullptr; i++)
        {
            if (EQUAL(papszTypes[i], "PO"))
                papszOSODRList = CSLAddString(papszOSODRList, papszValues[i]);
        }

        poFeature->SetField(9, papszOSODRList);
        CPLAssert(CSLCount(papszOSODRList) == poFeature->GetFieldAsInteger(8));

        CSLDestroy(papszOSODRList);
        CSLDestroy(papszTypes);
        CSLDestroy(papszValues);
    }

    return poFeature;
}

/************************************************************************/
/*                       TranslateOscarComment()                        */
/************************************************************************/

static OGRFeature *TranslateOscarComment(CPL_UNUSED NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 1 ||
        papoGroup[0]->GetType() != NRT_COMMENT)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // RECORD_TYPE
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 4)));

    // RECORD_ID
    poFeature->SetField(1, papoGroup[0]->GetField(5, 17));

    // CHANGE_TYPE
    poFeature->SetField(2, papoGroup[0]->GetField(18, 18));

    return poFeature;
}

/************************************************************************/
/*                     TranslateOscarNetworkPoint()                     */
/************************************************************************/

static OGRFeature *TranslateOscarNetworkPoint(NTFFileReader *poReader,
                                              OGRNTFLayer *poLayer,
                                              NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "OD", 3, "JN",
                                   4, "SN", 5, "RT", 6, NULL);

    return poFeature;
}

/************************************************************************/
/*                     TranslateOscarNetworkLine()                      */
/************************************************************************/

static OGRFeature *TranslateOscarNetworkLine(NTFFileReader *poReader,
                                             OGRNTFLayer *poLayer,
                                             NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "OD", 3, "PN",
                                   4, "LL", 5, "RN", 6, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateBasedataPoint()                       */
/************************************************************************/

static OGRFeature *TranslateBasedataPoint(NTFFileReader *poReader,
                                          OGRNTFLayer *poLayer,
                                          NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "PN", 3, "NU",
                                   4, "CM", 5, "UN", 6, "OR", 7, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateBasedataLine()                        */
/************************************************************************/

static OGRFeature *TranslateBasedataLine(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(2, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "PN", 3, "NU",
                                   4, "RB", 5, NULL);

    return poFeature;
}

/************************************************************************/
/*                  TranslateBoundarylineCollection()                   */
/************************************************************************/

static OGRFeature *TranslateBoundarylineCollection(NTFFileReader *poReader,
                                                   OGRNTFLayer *poLayer,
                                                   NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 2 ||
        papoGroup[0]->GetType() != NRT_COLLECT ||
        papoGroup[1]->GetType() != NRT_ATTREC)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // COLL_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // NUM_PARTS
    int nNumLinks = atoi(papoGroup[0]->GetField(9, 12));

    if (nNumLinks > MAX_LINK)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "MAX_LINK exceeded in ntf_estlayers.cpp.");
        return poFeature;
    }

    poFeature->SetField(1, nNumLinks);

    // POLY_ID
    int i, anList[MAX_LINK] = {0};

    for (i = 0; i < nNumLinks; i++)
        anList[i] = atoi(papoGroup[0]->GetField(15 + i * 8, 20 + i * 8));

    poFeature->SetField(2, nNumLinks, anList);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "AI", 3, "OP", 4, "NM",
                                   5, NULL);

    return poFeature;
}

/************************************************************************/
/*                     TranslateBoundarylinePoly()                      */
/************************************************************************/

static OGRFeature *TranslateBoundarylinePoly(NTFFileReader *poReader,
                                             OGRNTFLayer *poLayer,
                                             NTFRecord **papoGroup)

{
    /* ==================================================================== */
    /*      Traditional POLYGON record groups.                              */
    /* ==================================================================== */
    if (CSLCount((char **)papoGroup) == 4 &&
        papoGroup[0]->GetType() == NRT_POLYGON &&
        papoGroup[1]->GetType() == NRT_ATTREC &&
        papoGroup[2]->GetType() == NRT_CHAIN &&
        papoGroup[3]->GetType() == NRT_GEOMETRY)
    {

        OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

        // POLY_ID
        poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

        // NUM_PARTS
        int nNumLinks = atoi(papoGroup[2]->GetField(9, 12));

        if (nNumLinks > MAX_LINK)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "MAX_LINK exceeded in ntf_estlayers.cpp.");
            return poFeature;
        }

        poFeature->SetField(4, nNumLinks);

        // DIR
        int i, anList[MAX_LINK] = {0};

        for (i = 0; i < nNumLinks; i++)
            anList[i] = atoi(papoGroup[2]->GetField(19 + i * 7, 19 + i * 7));

        poFeature->SetField(5, nNumLinks, anList);

        // GEOM_ID_OF_LINK
        for (i = 0; i < nNumLinks; i++)
            anList[i] = atoi(papoGroup[2]->GetField(13 + i * 7, 18 + i * 7));

        poFeature->SetField(6, nNumLinks, anList);

        // RingStart
        int nRingList = 0;
        poFeature->SetField(7, 1, &nRingList);

        // Attributes
        poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "PI", 2,
                                       "HA", 3, NULL);

        // Read point geometry
        poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[3]));

        // Try to assemble polygon geometry.
        poReader->FormPolygonFromCache(poFeature);

        return poFeature;
    }

    /* ==================================================================== */
    /*      CPOLYGON Group                                                  */
    /* ==================================================================== */

    /* -------------------------------------------------------------------- */
    /*      First we do validation of the grouping.                         */
    /* -------------------------------------------------------------------- */
    int iRec = 0;  // Used after for.

    for (; papoGroup[iRec] != nullptr && papoGroup[iRec + 1] != nullptr &&
           papoGroup[iRec]->GetType() == NRT_POLYGON &&
           papoGroup[iRec + 1]->GetType() == NRT_CHAIN;
         iRec += 2)
    {
    }

    if (CSLCount((char **)papoGroup) != iRec + 3)
        return nullptr;

    if (papoGroup[iRec]->GetType() != NRT_CPOLY ||
        papoGroup[iRec + 1]->GetType() != NRT_ATTREC ||
        papoGroup[iRec + 2]->GetType() != NRT_GEOMETRY)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Collect the chains for each of the rings, and just aggregate    */
    /*      these into the master list without any concept of where the     */
    /*      boundaries are.  The boundary information will be emitted      */
    /*      in the RingStart field.                                         */
    /* -------------------------------------------------------------------- */
    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());
    int nNumLink = 0;
    int anDirList[MAX_LINK * 2] = {};
    int anGeomList[MAX_LINK * 2] = {};
    int anRingStart[MAX_LINK] = {};
    int nRings = 0;

    for (iRec = 0;
         papoGroup[iRec] != nullptr && papoGroup[iRec + 1] != nullptr &&
         papoGroup[iRec]->GetType() == NRT_POLYGON &&
         papoGroup[iRec + 1]->GetType() == NRT_CHAIN;
         iRec += 2)
    {
        const int nLineCount = atoi(papoGroup[iRec + 1]->GetField(9, 12));

        anRingStart[nRings++] = nNumLink;

        for (int i = 0; i < nLineCount && nNumLink < MAX_LINK * 2; i++)
        {
            anDirList[nNumLink] =
                atoi(papoGroup[iRec + 1]->GetField(19 + i * 7, 19 + i * 7));
            anGeomList[nNumLink] =
                atoi(papoGroup[iRec + 1]->GetField(13 + i * 7, 18 + i * 7));
            nNumLink++;
        }

        if (nNumLink == MAX_LINK * 2)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "MAX_LINK exceeded in ntf_estlayers.cpp.");

            delete poFeature;
            return nullptr;
        }
    }

    // NUM_PART
    poFeature->SetField(4, nNumLink);

    // DIR
    poFeature->SetField(5, nNumLink, anDirList);

    // GEOM_ID_OF_LINK
    poFeature->SetField(6, nNumLink, anGeomList);

    // RingStart
    poFeature->SetField(7, nRings, anRingStart);

    /* -------------------------------------------------------------------- */
    /*      collect information for whole complex polygon.                  */
    /* -------------------------------------------------------------------- */
    // POLY_ID
    if (papoGroup[iRec] != nullptr)
        poFeature->SetField(0, atoi(papoGroup[iRec]->GetField(3, 8)));

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "PI", 2, "HA",
                                   3, NULL);

    // point geometry for seed.
    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[iRec + 2]));

    // Try to assemble polygon geometry.
    poReader->FormPolygonFromCache(poFeature);

    return poFeature;
}

/************************************************************************/
/*                     TranslateBoundarylineLink()                      */
/************************************************************************/

static OGRFeature *TranslateBoundarylineLink(NTFFileReader *poReader,
                                             OGRNTFLayer *poLayer,
                                             NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 2 ||
        papoGroup[0]->GetType() != NRT_GEOMETRY ||
        papoGroup[1]->GetType() != NRT_ATTREC)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[0], &nGeomId));

    // GEOM_ID
    poFeature->SetField(0, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "LK", 2, "HW",
                                   3, NULL);

    return poFeature;
}

/************************************************************************/
/*                        TranslateBL2000Poly()                         */
/************************************************************************/

static OGRFeature *TranslateBL2000Poly(NTFFileReader *poReader,
                                       OGRNTFLayer *poLayer,
                                       NTFRecord **papoGroup)

{
    /* ==================================================================== */
    /*      Traditional POLYGON record groups.                              */
    /* ==================================================================== */
    if (CSLCount((char **)papoGroup) == 3 &&
        papoGroup[0]->GetType() == NRT_POLYGON &&
        papoGroup[1]->GetType() == NRT_ATTREC &&
        papoGroup[2]->GetType() == NRT_CHAIN)
    {

        OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

        // POLY_ID
        poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

        // NUM_PARTS
        int nNumLinks = atoi(papoGroup[2]->GetField(9, 12));

        if (nNumLinks > MAX_LINK)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "MAX_LINK exceeded in ntf_estlayers.cpp.");

            return poFeature;
        }

        poFeature->SetField(3, nNumLinks);

        // DIR
        int i, anList[MAX_LINK] = {0};

        for (i = 0; i < nNumLinks; i++)
            anList[i] = atoi(papoGroup[2]->GetField(19 + i * 7, 19 + i * 7));

        poFeature->SetField(4, nNumLinks, anList);

        // GEOM_ID_OF_LINK
        for (i = 0; i < nNumLinks; i++)
            anList[i] = atoi(papoGroup[2]->GetField(13 + i * 7, 18 + i * 7));

        poFeature->SetField(5, nNumLinks, anList);

        // RingStart
        int nRingList = 0;
        poFeature->SetField(6, 1, &nRingList);

        // Attributes
        poReader->ApplyAttributeValues(poFeature, papoGroup, "PI", 1, "HA", 2,
                                       NULL);

        // Try to assemble polygon geometry.
        poReader->FormPolygonFromCache(poFeature);

        return poFeature;
    }

    /* ==================================================================== */
    /*      CPOLYGON Group                                                  */
    /* ==================================================================== */

    /* -------------------------------------------------------------------- */
    /*      First we do validation of the grouping.                         */
    /* -------------------------------------------------------------------- */
    int iRec = 0;  // Used after for.

    for (; papoGroup[iRec] != nullptr && papoGroup[iRec + 1] != nullptr &&
           papoGroup[iRec]->GetType() == NRT_POLYGON &&
           papoGroup[iRec + 1]->GetType() == NRT_CHAIN;
         iRec += 2)
    {
    }

    if (CSLCount((char **)papoGroup) != iRec + 2)
        return nullptr;

    if (papoGroup[iRec]->GetType() != NRT_CPOLY ||
        papoGroup[iRec + 1]->GetType() != NRT_ATTREC)
        return nullptr;

    /* -------------------------------------------------------------------- */
    /*      Collect the chains for each of the rings, and just aggregate    */
    /*      these into the master list without any concept of where the     */
    /*      boundaries are.  The boundary information will be emitted      */
    /*      in the RingStart field.                                         */
    /* -------------------------------------------------------------------- */
    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());
    int nNumLink = 0;
    int anDirList[MAX_LINK * 2] = {};
    int anGeomList[MAX_LINK * 2] = {};
    int anRingStart[MAX_LINK] = {};
    int nRings = 0;

    for (iRec = 0;
         papoGroup[iRec] != nullptr && papoGroup[iRec + 1] != nullptr &&
         papoGroup[iRec]->GetType() == NRT_POLYGON &&
         papoGroup[iRec + 1]->GetType() == NRT_CHAIN;
         iRec += 2)
    {
        const int nLineCount = atoi(papoGroup[iRec + 1]->GetField(9, 12));

        anRingStart[nRings++] = nNumLink;

        for (int i = 0; i < nLineCount && nNumLink < MAX_LINK * 2; i++)
        {
            anDirList[nNumLink] =
                atoi(papoGroup[iRec + 1]->GetField(19 + i * 7, 19 + i * 7));
            anGeomList[nNumLink] =
                atoi(papoGroup[iRec + 1]->GetField(13 + i * 7, 18 + i * 7));
            nNumLink++;
        }

        if (nNumLink == MAX_LINK * 2)
        {
            CPLError(CE_Failure, CPLE_AppDefined,
                     "MAX_LINK exceeded in ntf_estlayers.cpp.");

            delete poFeature;
            return nullptr;
        }
    }

    // NUM_PART
    poFeature->SetField(3, nNumLink);

    // DIR
    poFeature->SetField(4, nNumLink, anDirList);

    // GEOM_ID_OF_LINK
    poFeature->SetField(5, nNumLink, anGeomList);

    // RingStart
    poFeature->SetField(6, nRings, anRingStart);

    /* -------------------------------------------------------------------- */
    /*      collect information for whole complex polygon.                  */
    /* -------------------------------------------------------------------- */
    // POLY_ID
    if (papoGroup[iRec] != nullptr)
        poFeature->SetField(0, atoi(papoGroup[iRec]->GetField(3, 8)));

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "PI", 1, "HA", 2,
                                   NULL);

    // Try to assemble polygon geometry.
    poReader->FormPolygonFromCache(poFeature);

    return poFeature;
}

/************************************************************************/
/*                        TranslateBL2000Link()                         */
/************************************************************************/

static OGRFeature *TranslateBL2000Link(NTFFileReader *poReader,
                                       OGRNTFLayer *poLayer,
                                       NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 3 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY ||
        papoGroup[2]->GetType() != NRT_ATTREC)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "LK", 3,
                                   NULL);

    return poFeature;
}

/************************************************************************/
/*                     TranslateBL2000Collection()                      */
/************************************************************************/

static OGRFeature *TranslateBL2000Collection(NTFFileReader *poReader,
                                             OGRNTFLayer *poLayer,
                                             NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_COLLECT ||
        papoGroup[1]->GetType() != NRT_ATTREC)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // COLL_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // NUM_PARTS
    int nNumLinks = atoi(papoGroup[0]->GetField(9, 12));

    if (nNumLinks > MAX_LINK)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "MAX_LINK exceeded in ntf_estlayers.cpp.");

        return poFeature;
    }

    poFeature->SetField(1, nNumLinks);

    // POLY_ID / COLL_ID_REFS
    int anList[MAX_LINK] = {0}, anCollList[MAX_LINK] = {0};
    int nPolys = 0, nCollections = 0;

    for (int i = 0; i < nNumLinks; i++)
    {
        if (atoi(papoGroup[0]->GetField(13 + i * 8, 14 + i * 8)) == 34)
            anCollList[nCollections++] =
                atoi(papoGroup[0]->GetField(15 + i * 8, 20 + i * 8));
        else
            anList[nPolys++] =
                atoi(papoGroup[0]->GetField(15 + i * 8, 20 + i * 8));
    }

    // coverity[uninit_use_in_call]
    poFeature->SetField(2, nPolys, anList);
    // coverity[uninit_use_in_call]
    poFeature->SetField(10, nCollections, anCollList);

    // Attributes
    // Node that _CODE_DESC values are automatically applied if
    // the target fields exist.
    poReader->ApplyAttributeValues(poFeature, papoGroup, "AI", 3, "OP", 4, "NM",
                                   5, "TY", 6, "AC", 7, "NB", 8, "NA", 9, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateMeridianPoint()                       */
/************************************************************************/

static OGRFeature *TranslateMeridianPoint(NTFFileReader *poReader,
                                          OGRNTFLayer *poLayer,
                                          NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "PN", 3, "OS",
                                   4, "JN", 5, "RT", 6, "SI", 7, "PI", 8, "NM",
                                   9, "DA", 10, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateMeridianLine()                        */
/************************************************************************/

static OGRFeature *TranslateMeridianLine(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(2, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "OM", 3, "RN",
                                   4, "TR", 5, "RI", 6, "LC", 7, "RC", 8, "LD",
                                   9, "RD", 10, NULL);

    return poFeature;
}

/************************************************************************/
/*                      TranslateMeridian2Point()                       */
/************************************************************************/

static OGRFeature *TranslateMeridian2Point(NTFFileReader *poReader,
                                           OGRNTFLayer *poLayer,
                                           NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(1, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 2, "PN", 3, "OD",
                                   4, "PO", 5, "JN", 6, "RT", 7, "SN", 8, "SI",
                                   9, "PI", 10, "NM", 11, "DA", 12, "WA", 13,
                                   "HT", 14, "FA", 15, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateMeridian2Line()                       */
/************************************************************************/

static OGRFeature *TranslateMeridian2Line(NTFFileReader *poReader,
                                          OGRNTFLayer *poLayer,
                                          NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(2, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(
        poFeature, papoGroup, "FC", 1, "OD", 3, "PO", 4, "RN", 5, "TR", 6, "PN",
        7, "RI", 8, "LC", 9, "RC", 10, "LD", 11, "RD", 12, "WI", 14, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateStrategiNode()                        */
/*                                                                      */
/*      Also used for Meridian, Oscar and BaseData.GB nodes.            */
/************************************************************************/

static OGRFeature *TranslateStrategiNode(CPL_UNUSED NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 1 ||
        papoGroup[0]->GetType() != NRT_NODEREC)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // NODE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // GEOM_ID_OF_POINT
    poFeature->SetField(1, atoi(papoGroup[0]->GetField(9, 14)));

    // NUM_LINKS
    int nNumLinks = atoi(papoGroup[0]->GetField(15, 18));

    if (nNumLinks < 0 || nNumLinks > MAX_LINK)
    {
        CPLError(CE_Failure, CPLE_AppDefined,
                 "MAX_LINK exceeded in ntf_estlayers.cpp.");

        return poFeature;
    }

    poFeature->SetField(2, nNumLinks);

    // DIR
    int i, anList[MAX_LINK] = {0};

    for (i = 0; i < nNumLinks; i++)
        anList[i] = atoi(papoGroup[0]->GetField(19 + i * 12, 19 + i * 12));

    poFeature->SetField(3, nNumLinks, anList);

    // GEOM_ID_OF_POINT
    for (i = 0; i < nNumLinks; i++)
        anList[i] =
            atoi(papoGroup[0]->GetField(19 + i * 12 + 1, 19 + i * 12 + 6));

    poFeature->SetField(4, nNumLinks, anList);

    // LEVEL
    for (i = 0; i < nNumLinks; i++)
        anList[i] =
            atoi(papoGroup[0]->GetField(19 + i * 12 + 11, 19 + i * 12 + 11));

    poFeature->SetField(5, nNumLinks, anList);

    // ORIENT (optional)
    if (EQUAL(poFeature->GetDefnRef()->GetFieldDefn(6)->GetNameRef(), "ORIENT"))
    {
        double adfList[MAX_LINK] = {0};

        for (i = 0; i < nNumLinks; i++)
            adfList[i] = atoi(papoGroup[0]->GetField(19 + i * 12 + 7,
                                                     19 + i * 12 + 10)) *
                         0.1;

        poFeature->SetField(6, nNumLinks, adfList);
    }

    return poFeature;
}

/************************************************************************/
/*                       TranslateStrategiText()                        */
/*                                                                      */
/*      Also used for Meridian, BaseData and Generic text.              */
/************************************************************************/

static OGRFeature *TranslateStrategiText(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 4 ||
        papoGroup[0]->GetType() != NRT_TEXTREC ||
        papoGroup[1]->GetType() != NRT_TEXTPOS ||
        papoGroup[2]->GetType() != NRT_TEXTREP ||
        papoGroup[3]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FONT
    poFeature->SetField(2, atoi(papoGroup[2]->GetField(9, 12)));

    // TEXT_HT
    poFeature->SetField(3, atoi(papoGroup[2]->GetField(13, 15)) * 0.1);

    // DIG_POSTN
    poFeature->SetField(4, atoi(papoGroup[2]->GetField(16, 16)));

    // ORIENT
    poFeature->SetField(5, atoi(papoGroup[2]->GetField(17, 20)) * 0.1);

    // TEXT_HT_GROUND
    poFeature->SetField(7, poFeature->GetFieldAsDouble(3) *
                               poReader->GetPaperToGround());

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[3]));

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "TX", 6, "DE",
                                   8, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateStrategiPoint()                       */
/************************************************************************/

static OGRFeature *TranslateStrategiPoint(NTFFileReader *poReader,
                                          OGRNTFLayer *poLayer,
                                          NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(10, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(
        poFeature, papoGroup, "FC", 1, "PN", 2, "NU", 3, "RB", 4, "RU", 5, "AN",
        6, "AO", 7, "CM", 8, "UN", 9, "DE", 11, "DN", 12, "FM", 13, "GS", 14,
        "HI", 15, "HM", 16, "LO", 17, "OR", 18, "OW", 19, "RJ", 20, "RL", 21,
        "RM", 22, "RQ", 23, "RW", 24, "RZ", 25, "UE", 26, NULL);

    return poFeature;
}

/************************************************************************/
/*                       TranslateStrategiLine()                        */
/************************************************************************/

static OGRFeature *TranslateStrategiLine(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // Geometry
    int nGeomId = 0;

    poFeature->SetGeometryDirectly(
        poReader->ProcessGeometry(papoGroup[1], &nGeomId));

    // GEOM_ID
    poFeature->SetField(3, nGeomId);

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "FC", 1, "PN", 2, "DE",
                                   4, "FE", 5, "FF", 6, "FI", 7, "FM", 8, "FP",
                                   9, "FR", 10, "FT", 11, "GS", 12, "NU", 13,
                                   "TX", 14, NULL);

    return poFeature;
}

/************************************************************************/
/*                      TranslateLandrangerPoint()                      */
/************************************************************************/

static OGRFeature *TranslateLandrangerPoint(NTFFileReader *poReader,
                                            OGRNTFLayer *poLayer,
                                            NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FEAT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(17, 20));

    // HEIGHT
    poFeature->SetField(2, atoi(papoGroup[0]->GetField(11, 16)));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    return poFeature;
}

/************************************************************************/
/*                      TranslateLandrangerLine()                       */
/************************************************************************/

static OGRFeature *TranslateLandrangerLine(NTFFileReader *poReader,
                                           OGRNTFLayer *poLayer,
                                           NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FEAT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(17, 20));

    // HEIGHT
    poFeature->SetField(2, atoi(papoGroup[0]->GetField(11, 16)));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    return poFeature;
}

/************************************************************************/
/*                       TranslateProfilePoint()                        */
/************************************************************************/

static OGRFeature *TranslateProfilePoint(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        (papoGroup[1]->GetType() != NRT_GEOMETRY &&
         papoGroup[1]->GetType() != NRT_GEOMETRY3D))
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FEAT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(17, 20));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "HT", 2, NULL);

    // Set HEIGHT/elevation
    OGRPoint *poPoint = dynamic_cast<OGRPoint *>(poFeature->GetGeometryRef());

    if (poPoint != nullptr && poPoint->getCoordinateDimension() == 3)
    {
        poFeature->SetField(2, poPoint->getZ());
    }
    else if (poPoint != nullptr)
    {
        poFeature->SetField(2, poFeature->GetFieldAsDouble(2) * 0.01);
        poPoint->setZ(poFeature->GetFieldAsDouble(2));
    }

    return poFeature;
}

/************************************************************************/
/*                        TranslateProfileLine()                        */
/************************************************************************/

static OGRFeature *TranslateProfileLine(NTFFileReader *poReader,
                                        OGRNTFLayer *poLayer,
                                        NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        (papoGroup[1]->GetType() != NRT_GEOMETRY &&
         papoGroup[1]->GetType() != NRT_GEOMETRY3D))
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FEAT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(17, 20));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    // Attributes
    poReader->ApplyAttributeValues(poFeature, papoGroup, "HT", 2, NULL);

    // Set HEIGHT/elevation
    OGRLineString *poLine =
        dynamic_cast<OGRLineString *>(poFeature->GetGeometryRef());

    poFeature->SetField(2, poFeature->GetFieldAsDouble(2) * 0.01);
    if (poLine != nullptr && poLine->getCoordinateDimension() == 2)
    {
        for (int i = 0; i < poLine->getNumPoints(); i++)
        {
            poLine->setPoint(i, poLine->getX(i), poLine->getY(i),
                             poFeature->GetFieldAsDouble(2));
        }
    }
    else if (poLine != nullptr)
    {
        double dfAccum = 0.0;

        for (int i = 0; i < poLine->getNumPoints(); i++)
        {
            dfAccum += poLine->getZ(i);
        }
        poFeature->SetField(2, dfAccum / poLine->getNumPoints());
    }

    return poFeature;
}

/************************************************************************/
/*                       TranslateLandlinePoint()                       */
/************************************************************************/

static OGRFeature *TranslateLandlinePoint(NTFFileReader *poReader,
                                          OGRNTFLayer *poLayer,
                                          NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) < 2 ||
        papoGroup[0]->GetType() != NRT_POINTREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // POINT_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FEAT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(17, 20));

    // ORIENT
    poFeature->SetField(2, atoi(papoGroup[0]->GetField(11, 16)) * 0.1);

    // DISTANCE
    poReader->ApplyAttributeValues(poFeature, papoGroup, "DT", 3, NULL);

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    // CHG_DATE (optional)
    if (poFeature->GetFieldIndex("CHG_DATE") == 4)
    {
        poFeature->SetField(4, papoGroup[0]->GetField(23, 28));
    }

    // CHG_TYPE (optional)
    if (poFeature->GetFieldIndex("CHG_TYPE") == 5)
    {
        poFeature->SetField(5, papoGroup[0]->GetField(22, 22));
    }

    return poFeature;
}

/************************************************************************/
/*                       TranslateLandlineLine()                        */
/************************************************************************/

static OGRFeature *TranslateLandlineLine(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 2 ||
        papoGroup[0]->GetType() != NRT_LINEREC ||
        papoGroup[1]->GetType() != NRT_GEOMETRY)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // LINE_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // FEAT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(17, 20));

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[1]));

    // CHG_DATE (optional)
    if (poFeature->GetFieldIndex("CHG_DATE") == 2)
    {
        poFeature->SetField(2, papoGroup[0]->GetField(23, 28));
    }

    // CHG_TYPE (optional)
    if (poFeature->GetFieldIndex("CHG_TYPE") == 3)
    {
        poFeature->SetField(3, papoGroup[0]->GetField(22, 22));
    }
    return poFeature;
}

/************************************************************************/
/*                       TranslateLandlineName()                        */
/************************************************************************/

static OGRFeature *TranslateLandlineName(NTFFileReader *poReader,
                                         OGRNTFLayer *poLayer,
                                         NTFRecord **papoGroup)

{
    if (CSLCount((char **)papoGroup) != 3 ||
        papoGroup[0]->GetType() != NRT_NAMEREC ||
        papoGroup[1]->GetType() != NRT_NAMEPOSTN ||
        papoGroup[2]->GetType() != NRT_GEOMETRY)
        return nullptr;

    int nNumChar = atoi(papoGroup[0]->GetField(13, 14));
    if (nNumChar <= 0)
        return nullptr;

    OGRFeature *poFeature = new OGRFeature(poLayer->GetLayerDefn());

    // NAME_ID
    poFeature->SetField(0, atoi(papoGroup[0]->GetField(3, 8)));

    // TEXT_CODE
    poFeature->SetField(1, papoGroup[0]->GetField(9, 12));

    // TEXT
    poFeature->SetField(2, papoGroup[0]->GetField(15, 15 + nNumChar - 1));

    // FONT
    poFeature->SetField(3, atoi(papoGroup[1]->GetField(3, 6)));

    // TEXT_HT
    poFeature->SetField(4, atoi(papoGroup[1]->GetField(7, 9)) * 0.1);

    // DIG_POSTN
    poFeature->SetField(5, atoi(papoGroup[1]->GetField(10, 10)));

    // ORIENT
    poFeature->SetField(6, CPLAtof(papoGroup[1]->GetField(11, 14)) * 0.1);

    // TEXT_HT_GROUND
    poFeature->SetField(7, poFeature->GetFieldAsDouble(4) *
                               poReader->GetPaperToGround());

    // CHG_DATE (optional)
    if (poFeature->GetFieldIndex("CHG_DATE") == 7)
    {
        poFeature->SetField(8, papoGroup[0]->GetField(15 + nNumChar + 2,
                                                      15 + nNumChar + 2 + 5));
    }

    // CHG_TYPE (optional)
    if (poFeature->GetFieldIndex("CHG_TYPE") == 9)
    {
        poFeature->SetField(
            9, papoGroup[0]->GetField(15 + nNumChar + 1, 15 + nNumChar + 1));
    }

    // Geometry
    poFeature->SetGeometryDirectly(poReader->ProcessGeometry(papoGroup[2]));

    return poFeature;
}

/************************************************************************/
/*                           EstablishLayer()                           */
/*                                                                      */
/*      Establish one layer based on a simplified description of the    */
/*      fields to be present.                                           */
/************************************************************************/

void NTFFileReader::EstablishLayer(const char *pszLayerName,
                                   OGRwkbGeometryType eGeomType,
                                   NTFFeatureTranslator pfnTranslator,
                                   int nLeadRecordType,
                                   NTFGenericClass *poClass, ...)

{
    /* -------------------------------------------------------------------- */
    /*      Does this layer already exist?  If so, we do nothing            */
    /*      ... note that we don't check the definition.                    */
    /* -------------------------------------------------------------------- */
    OGRNTFLayer *poLayer = poDS->GetNamedLayer(pszLayerName);

    /* ==================================================================== */
    /*      Create a new layer matching the request if we don't already      */
    /*      have one.                                                       */
    /* ==================================================================== */
    if (poLayer == nullptr)
    {
        /* --------------------------------------------------------------------
         */
        /*      Create a new feature definition. */
        /* --------------------------------------------------------------------
         */
        OGRFeatureDefn *poDefn = new OGRFeatureDefn(pszLayerName);
        poDefn->GetGeomFieldDefn(0)->SetSpatialRef(poDS->DSGetSpatialRef());
        poDefn->SetGeomType(eGeomType);
        poDefn->Reference();

        /* --------------------------------------------------------------------
         */
        /*      Fetch definitions of each field in turn. */
        /* --------------------------------------------------------------------
         */
        va_list hVaArgs;
        va_start(hVaArgs, poClass);
        while (true)
        {
            const char *pszFieldName = va_arg(hVaArgs, const char *);

            if (pszFieldName == nullptr)
                break;

            const OGRFieldType eType = (OGRFieldType)va_arg(hVaArgs, int);
            const int nWidth = va_arg(hVaArgs, int);
            const int nPrecision = va_arg(hVaArgs, int);

            OGRFieldDefn oFieldDefn(pszFieldName, eType);
            oFieldDefn.SetWidth(nWidth);
            oFieldDefn.SetPrecision(nPrecision);

            poDefn->AddFieldDefn(&oFieldDefn);
        }

        va_end(hVaArgs);

        /* --------------------------------------------------------------------
         */
        /*      Add attributes collected in the generic class survey. */
        /* --------------------------------------------------------------------
         */
        if (poClass != nullptr)
        {
            for (int iGAtt = 0; iGAtt < poClass->nAttrCount; iGAtt++)
            {
                const char *pszFormat = poClass->papszAttrFormats[iGAtt];
                OGRFieldDefn oFieldDefn(poClass->papszAttrNames[iGAtt],
                                        OFTInteger);

                if (STARTS_WITH_CI(pszFormat, "I"))
                {
                    oFieldDefn.SetType(OFTInteger);
                    oFieldDefn.SetWidth(poClass->panAttrMaxWidth[iGAtt]);
                }
                else if (STARTS_WITH_CI(pszFormat, "D") ||
                         STARTS_WITH_CI(pszFormat, "A"))
                {
                    oFieldDefn.SetType(OFTString);
                    oFieldDefn.SetWidth(poClass->panAttrMaxWidth[iGAtt]);
                }
                else if (STARTS_WITH_CI(pszFormat, "R"))
                {
                    oFieldDefn.SetType(OFTReal);
                    oFieldDefn.SetWidth(poClass->panAttrMaxWidth[iGAtt] + 1);
                    const size_t nFormatLen = strlen(pszFormat);
                    if (nFormatLen >= 4 && pszFormat[2] == ',')
                        oFieldDefn.SetPrecision(atoi(pszFormat + 3));
                    else if (nFormatLen >= 5 && pszFormat[3] == ',')
                        oFieldDefn.SetPrecision(atoi(pszFormat + 4));
                }

                poDefn->AddFieldDefn(&oFieldDefn);

                /*
                ** If this field can appear multiple times, create an
                ** additional attribute to hold lists of values.  This
                ** is always created as a variable length string field.
                */
                if (poClass->pabAttrMultiple[iGAtt])
                {
                    char szName[128];

                    snprintf(szName, sizeof(szName), "%s_LIST",
                             poClass->papszAttrNames[iGAtt]);

                    OGRFieldDefn oFieldDefnL(szName, OFTString);

                    poDefn->AddFieldDefn(&oFieldDefnL);
                }
            }
        }

        /* --------------------------------------------------------------------
         */
        /*      Add the TILE_REF attribute. */
        /* --------------------------------------------------------------------
         */
        OGRFieldDefn oTileID("TILE_REF", OFTString);

        oTileID.SetWidth(10);

        poDefn->AddFieldDefn(&oTileID);

        /* --------------------------------------------------------------------
         */
        /*      Create the layer, and give over to the data source object to */
        /*      maintain. */
        /* --------------------------------------------------------------------
         */
        poLayer = new OGRNTFLayer(poDS, poDefn, pfnTranslator);

        poDS->AddLayer(poLayer);
    }

    /* -------------------------------------------------------------------- */
    /*      Register this translator with this file reader for handling     */
    /*      the indicate record type.                                       */
    /* -------------------------------------------------------------------- */
    apoTypeTranslation[nLeadRecordType] = poLayer;
}

/************************************************************************/
/*                          EstablishLayers()                           */
/*                                                                      */
/*      This method is responsible for creating any missing             */
/*      OGRNTFLayers needed for the current product based on the        */
/*      product name.                                                   */
/*                                                                      */
/*      NOTE: Any changes to the order of attribute fields in the       */
/*      following EstablishLayer() calls must also result in updates    */
/*      to the translate functions.  Changes of names, widths and to    */
/*      some extent types can be done without side effects.             */
/************************************************************************/

void NTFFileReader::EstablishLayers()

{
    if (poDS == nullptr || fp == nullptr)
        return;

    if (GetProductId() == NPC_LANDLINE)
    {
        EstablishLayer("LANDLINE_POINT", wkbPoint, TranslateLandlinePoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "ORIENT", OFTReal, 5, 1,
                       "DISTANCE", OFTReal, 6, 3, NULL);

        EstablishLayer("LANDLINE_LINE", wkbLineString, TranslateLandlineLine,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, NULL);

        EstablishLayer("LANDLINE_NAME", wkbPoint, TranslateLandlineName,
                       NRT_NAMEREC, nullptr, "NAME_ID", OFTInteger, 6, 0,
                       "TEXT_CODE", OFTString, 4, 0, "TEXT", OFTString, 0, 0,
                       "FONT", OFTInteger, 4, 0, "TEXT_HT", OFTReal, 4, 1,
                       "DIG_POSTN", OFTInteger, 1, 0, "ORIENT", OFTReal, 5, 1,
                       "TEXT_HT_GROUND", OFTReal, 10, 3, NULL);
    }
    else if (GetProductId() == NPC_LANDLINE99)
    {
        EstablishLayer("LANDLINE99_POINT", wkbPoint, TranslateLandlinePoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "ORIENT", OFTReal, 5, 1,
                       "DISTANCE", OFTReal, 6, 3, "CHG_DATE", OFTString, 6, 0,
                       "CHG_TYPE", OFTString, 1, 0, NULL);

        EstablishLayer("LANDLINE99_LINE", wkbLineString, TranslateLandlineLine,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "CHG_DATE", OFTString, 6,
                       0, "CHG_TYPE", OFTString, 1, 0, NULL);

        EstablishLayer("LANDLINE99_NAME", wkbPoint, TranslateLandlineName,
                       NRT_NAMEREC, nullptr, "NAME_ID", OFTInteger, 6, 0,
                       "TEXT_CODE", OFTString, 4, 0, "TEXT", OFTString, 0, 0,
                       "FONT", OFTInteger, 4, 0, "TEXT_HT", OFTReal, 4, 1,
                       "DIG_POSTN", OFTInteger, 1, 0, "ORIENT", OFTReal, 5, 1,
                       "TEXT_HT_GROUND", OFTReal, 10, 3, "CHG_DATE", OFTString,
                       6, 0, "CHG_TYPE", OFTString, 1, 0, NULL);
    }
    else if (GetProductId() == NPC_LANDRANGER_CONT)
    {
        EstablishLayer("PANORAMA_POINT", wkbPoint, TranslateLandrangerPoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "HEIGHT", OFTReal, 7, 2,
                       NULL);

        EstablishLayer("PANORAMA_CONTOUR", wkbLineString,
                       TranslateLandrangerLine, NRT_LINEREC, nullptr, "LINE_ID",
                       OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4, 0, "HEIGHT",
                       OFTReal, 7, 2, NULL);
    }
    else if (GetProductId() == NPC_LANDFORM_PROFILE_CONT)
    {
        EstablishLayer("PROFILE_POINT", wkbPoint25D, TranslateProfilePoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "HEIGHT", OFTReal, 7, 2,
                       NULL);

        EstablishLayer("PROFILE_LINE", wkbLineString25D, TranslateProfileLine,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "HEIGHT", OFTReal, 7, 2,
                       NULL);
    }
    else if (GetProductId() == NPC_STRATEGI)
    {
        EstablishLayer(
            "STRATEGI_POINT", wkbPoint, TranslateStrategiPoint, NRT_POINTREC,
            nullptr, "POINT_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4, 0,
            "PROPER_NAME", OFTString, 0, 0, "FEATURE_NUMBER", OFTString, 0, 0,
            "RB", OFTString, 1, 0, "RU", OFTString, 1, 0, "AN", OFTString, 0, 0,
            "AO", OFTString, 0, 0, "COUNTY_NAME", OFTString, 0, 0,
            "UNITARY_NAME", OFTString, 0, 0, "GEOM_ID", OFTInteger, 6, 0,
            "DATE", OFTInteger, 8, 0, "DISTRICT_NAME", OFTString, 0, 0,
            "FEATURE_NAME", OFTString, 0, 0, "GIS", OFTString, 0, 0,
            "HEIGHT_IMPERIAL", OFTInteger, 4, 0, "HEIGHT_METRIC", OFTInteger, 4,
            0, "LOCATION", OFTInteger, 1, 0, "ORIENTATION", OFTReal, 4, 1,
            "OWNER", OFTString, 0, 0, "RESTRICTION_NORTH", OFTString, 0, 0,
            "RESTRICTION_SOUTH", OFTString, 0, 0, "RESTRICTION_EAST", OFTString,
            0, 0, "RESTRICTION_WEST", OFTString, 0, 0, "RESTRICTION_CLOCKWISE",
            OFTString, 0, 0, "RESTRICTION_ANTICLOCKWISE", OFTString, 0, 0,
            "USAGE", OFTInteger, 1, 0, NULL);

        EstablishLayer(
            "STRATEGI_LINE", wkbLineString, TranslateStrategiLine, NRT_LINEREC,
            nullptr, "LINE_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4, 0,
            "PROPER_NAME", OFTString, 0, 0, "GEOM_ID", OFTInteger, 6, 0, "DATE",
            OFTInteger, 8, 0, "FERRY_ACCESS", OFTString, 0, 0, "FERRY_FROM",
            OFTString, 0, 0, "FERRY_TIME", OFTString, 0, 0, "FEATURE_NAME",
            OFTString, 0, 0, "FERRY_TYPE", OFTString, 0, 0,
            "FERRY_RESTRICTIONS", OFTString, 0, 0, "FERRY_TO", OFTString, 0, 0,
            "GIS", OFTString, 0, 0, "FEATURE_NUMBER", OFTString, 0, 0, NULL);

        EstablishLayer(
            "STRATEGI_TEXT", wkbPoint, TranslateStrategiText, NRT_TEXTREC,
            nullptr, "TEXT_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4, 0,
            "FONT", OFTInteger, 4, 0, "TEXT_HT", OFTReal, 5, 1, "DIG_POSTN",
            OFTInteger, 1, 0, "ORIENT", OFTReal, 5, 1, "TEXT", OFTString, 0, 0,
            "TEXT_HT_GROUND", OFTReal, 10, 3, "DATE", OFTInteger, 8, 0, NULL);

        EstablishLayer("STRATEGI_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, "ORIENT", OFTRealList, 5, 1, NULL);
    }
    else if (GetProductId() == NPC_MERIDIAN)
    {
        EstablishLayer("MERIDIAN_POINT", wkbPoint, TranslateMeridianPoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "GEOM_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4,
                       0, "PROPER_NAME", OFTString, 0, 0, "OSMDR", OFTString,
                       13, 0, "JUNCTION_NAME", OFTString, 0, 0, "ROUNDABOUT",
                       OFTString, 1, 0, "STATION_ID", OFTString, 13, 0,
                       "GLOBAL_ID", OFTInteger, 6, 0, "ADMIN_NAME", OFTString,
                       0, 0, "DA_DLUA_ID", OFTString, 13, 0, NULL);

        EstablishLayer("MERIDIAN_LINE", wkbLineString, TranslateMeridianLine,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "GEOM_ID", OFTInteger, 6,
                       0, "OSMDR", OFTString, 13, 0, "ROAD_NUM", OFTString, 0,
                       0, "TRUNK_ROAD", OFTString, 1, 0, "RAIL_ID", OFTString,
                       13, 0, "LEFT_COUNTY", OFTInteger, 6, 0, "RIGHT_COUNTY",
                       OFTInteger, 6, 0, "LEFT_DISTRICT", OFTInteger, 6, 0,
                       "RIGHT_DISTRICT", OFTInteger, 6, 0, NULL);

        EstablishLayer("MERIDIAN_TEXT", wkbPoint, TranslateStrategiText,
                       NRT_TEXTREC, nullptr, "TEXT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "FONT", OFTInteger, 4, 0,
                       "TEXT_HT", OFTReal, 5, 1, "DIG_POSTN", OFTInteger, 1, 0,
                       "ORIENT", OFTReal, 5, 1, "TEXT", OFTString, 0, 0,
                       "TEXT_HT_GROUND", OFTReal, 10, 3, NULL);

        EstablishLayer("MERIDIAN_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, "ORIENT", OFTRealList, 5, 1, NULL);
    }
    else if (GetProductId() == NPC_MERIDIAN2)
    {
        EstablishLayer(
            "MERIDIAN2_POINT", wkbPoint, TranslateMeridian2Point, NRT_POINTREC,
            nullptr, "POINT_ID", OFTInteger, 6, 0, "GEOM_ID", OFTInteger, 6, 0,
            "FEAT_CODE", OFTString, 4, 0, "PROPER_NAME", OFTString, 0, 0,
            "OSODR", OFTString, 13, 0, "PARENT_OSODR", OFTString, 13, 0,
            "JUNCTION_NAME", OFTString, 0, 0, "ROUNDABOUT", OFTString, 1, 0,
            "SETTLEMENT_NAME", OFTString, 0, 0, "STATION_ID", OFTString, 13, 0,
            "GLOBAL_ID", OFTInteger, 6, 0, "ADMIN_NAME", OFTString, 0, 0,
            "DA_DLUA_ID", OFTString, 13, 0, "WATER_AREA", OFTString, 13, 0,
            "HEIGHT", OFTInteger, 8, 0, "FOREST_ID", OFTString, 13, 0, NULL);

        EstablishLayer("MERIDIAN2_LINE", wkbLineString, TranslateMeridian2Line,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "GEOM_ID", OFTInteger, 6,
                       0, "OSODR", OFTString, 13, 0, "PARENT_OSODR", OFTString,
                       13, 0, "ROAD_NUM", OFTString, 0, 0, "TRUNK_ROAD",
                       OFTString, 1, 0, "PROPER_NAME", OFTString, 0, 0,
                       "RAIL_ID", OFTString, 13, 0, "LEFT_COUNTY", OFTInteger,
                       6, 0, "RIGHT_COUNTY", OFTInteger, 6, 0, "LEFT_DISTRICT",
                       OFTInteger, 6, 0, "RIGHT_DISTRICT", OFTInteger, 6, 0,
                       "WATER_LINK_ID", OFTString, 13, 0, NULL);

        EstablishLayer("MERIDIAN2_TEXT", wkbPoint, TranslateStrategiText,
                       NRT_TEXTREC, nullptr, "TEXT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "FONT", OFTInteger, 4, 0,
                       "TEXT_HT", OFTReal, 5, 1, "DIG_POSTN", OFTInteger, 1, 0,
                       "ORIENT", OFTReal, 5, 1, "TEXT", OFTString, 0, 0,
                       "TEXT_HT_GROUND", OFTReal, 10, 3, NULL);

        EstablishLayer("MERIDIAN2_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, "ORIENT", OFTRealList, 5, 1, NULL);
    }
    else if (GetProductId() == NPC_BOUNDARYLINE)
    {
        EstablishLayer("BOUNDARYLINE_LINK", wkbLineString,
                       TranslateBoundarylineLink, NRT_GEOMETRY, nullptr,
                       "GEOM_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4,
                       0, "GLOBAL_LINK_ID", OFTInteger, 10, 0, "HWM_FLAG",
                       OFTInteger, 1, 0, NULL);

        EstablishLayer("BOUNDARYLINE_POLY", bCacheLines ? wkbPolygon : wkbPoint,
                       TranslateBoundarylinePoly, NRT_POLYGON, nullptr,
                       "POLY_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4,
                       0, "GLOBAL_SEED_ID", OFTInteger, 6, 0, "HECTARES",
                       OFTReal, 9, 3, "NUM_PARTS", OFTInteger, 4, 0, "DIR",
                       OFTIntegerList, 1, 0, "GEOM_ID_OF_LINK", OFTIntegerList,
                       6, 0, "RingStart", OFTIntegerList, 6, 0, NULL);

        EstablishLayer("BOUNDARYLINE_COLLECTIONS", wkbNone,
                       TranslateBoundarylineCollection, NRT_COLLECT, nullptr,
                       "COLL_ID", OFTInteger, 6, 0, "NUM_PARTS", OFTInteger, 4,
                       0, "POLY_ID", OFTIntegerList, 6, 0, "ADMIN_AREA_ID",
                       OFTInteger, 6, 0, "OPCS_CODE", OFTString, 6, 0,
                       "ADMIN_NAME", OFTString, 0, 0, NULL);
    }
    else if (GetProductId() == NPC_BL2000)
    {
        EstablishLayer("BL2000_LINK", wkbLineString, TranslateBL2000Link,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "GEOM_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4,
                       0, "GLOBAL_LINK_ID", OFTInteger, 10, 0, NULL);
        EstablishLayer("BL2000_POLY", bCacheLines ? wkbPolygon : wkbNone,
                       TranslateBL2000Poly, NRT_POLYGON, nullptr, "POLY_ID",
                       OFTInteger, 6, 0, "GLOBAL_SEED_ID", OFTInteger, 6, 0,
                       "HECTARES", OFTReal, 12, 3, "NUM_PARTS", OFTInteger, 4,
                       0, "DIR", OFTIntegerList, 1, 0, "GEOM_ID_OF_LINK",
                       OFTIntegerList, 6, 0, "RingStart", OFTIntegerList, 6, 0,
                       NULL);
        if (poDS->GetOption("CODELIST") != nullptr &&
            EQUAL(poDS->GetOption("CODELIST"), "ON"))
            EstablishLayer(
                "BL2000_COLLECTIONS", wkbNone, TranslateBL2000Collection,
                NRT_COLLECT, nullptr, "COLL_ID", OFTInteger, 6, 0, "NUM_PARTS",
                OFTInteger, 4, 0, "POLY_ID", OFTIntegerList, 6, 0,
                "ADMIN_AREA_ID", OFTInteger, 6, 0, "CENSUS_CODE", OFTString, 7,
                0, "ADMIN_NAME", OFTString, 0, 0, "AREA_TYPE", OFTString, 2, 0,
                "AREA_CODE", OFTString, 3, 0, "NON_TYPE_CODE", OFTString, 3, 0,
                "NON_INLAND_AREA", OFTReal, 12, 3, "COLL_ID_REFS",
                OFTIntegerList, 6, 0, "AREA_TYPE_DESC", OFTString, 0, 0,
                "AREA_CODE_DESC", OFTString, 0, 0, "NON_TYPE_CODE_DESC",
                OFTString, 0, 0, NULL);
        else
            EstablishLayer(
                "BL2000_COLLECTIONS", wkbNone, TranslateBL2000Collection,
                NRT_COLLECT, nullptr, "COLL_ID", OFTInteger, 6, 0, "NUM_PARTS",
                OFTInteger, 4, 0, "POLY_ID", OFTIntegerList, 6, 0,
                "ADMIN_AREA_ID", OFTInteger, 6, 0, "CENSUS_CODE", OFTString, 7,
                0, "ADMIN_NAME", OFTString, 0, 0, "AREA_TYPE", OFTString, 2, 0,
                "AREA_CODE", OFTString, 3, 0, "NON_TYPE_CODE", OFTString, 3, 0,
                "NON_INLAND_AREA", OFTReal, 12, 3, "COLL_ID_REFS",
                OFTIntegerList, 6, 0, NULL);
    }
    else if (GetProductId() == NPC_BASEDATA)
    {
        EstablishLayer(
            "BASEDATA_POINT", wkbPoint, TranslateBasedataPoint, NRT_POINTREC,
            nullptr, "POINT_ID", OFTInteger, 6, 0, "GEOM_ID", OFTInteger, 6, 0,
            "FEAT_CODE", OFTString, 4, 0, "PROPER_NAME", OFTString, 0, 0,
            "FEATURE_NUMBER", OFTString, 0, 0, "COUNTY_NAME", OFTString, 0, 0,
            "UNITARY_NAME", OFTString, 0, 0, "ORIENT", OFTRealList, 5, 1, NULL);

        EstablishLayer("BASEDATA_LINE", wkbLineString, TranslateBasedataLine,
                       NRT_LINEREC, nullptr, "LINE_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "GEOM_ID", OFTInteger, 6,
                       0, "PROPER_NAME", OFTString, 0, 0, "FEATURE_NUMBER",
                       OFTString, 0, 0, "RB", OFTString, 1, 0, NULL);

        EstablishLayer("BASEDATA_TEXT", wkbPoint, TranslateStrategiText,
                       NRT_TEXTREC, nullptr, "TEXT_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "FONT", OFTInteger, 4, 0,
                       "TEXT_HT", OFTReal, 5, 1, "DIG_POSTN", OFTInteger, 1, 0,
                       "ORIENT", OFTReal, 5, 1, "TEXT", OFTString, 0, 0,
                       "TEXT_HT_GROUND", OFTReal, 10, 3, NULL);

        EstablishLayer("BASEDATA_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, "ORIENT", OFTRealList, 5, 1, NULL);
    }
    else if (GetProductId() == NPC_OSCAR_ASSET ||
             GetProductId() == NPC_OSCAR_TRAFFIC)
    {
        EstablishLayer("OSCAR_POINT", wkbPoint, TranslateOscarPoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "GEOM_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4,
                       0, "OSODR", OFTString, 13, 0, "JUNCTION_NAME", OFTString,
                       0, 0, "SETTLE_NAME", OFTString, 0, 0, NULL);

        EstablishLayer(
            "OSCAR_LINE", wkbLineString, TranslateOscarLine, NRT_LINEREC,
            nullptr, "LINE_ID", OFTInteger, 6, 0, "GEOM_ID", OFTInteger, 6, 0,
            "FEAT_CODE", OFTString, 4, 0, "OSODR", OFTString, 13, 0,
            "PROPER_NAME", OFTString, 0, 0, "LINE_LENGTH", OFTInteger, 5, 0,
            "SOURCE", OFTString, 1, 0, "FORM_OF_WAY", OFTString, 1, 0,
            "ROAD_NUM", OFTString, 0, 0, "TRUNK_ROAD", OFTString, 1, 0, NULL);

        EstablishLayer("OSCAR_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, NULL);

        EstablishLayer("OSCAR_COMMENT", wkbNone, TranslateOscarComment,
                       NRT_COMMENT, nullptr, "RECORD_TYPE", OFTInteger, 2, 0,
                       "RECORD_ID", OFTString, 13, 0, "CHANGE_TYPE", OFTString,
                       1, 0, NULL);
    }
    else if (GetProductId() == NPC_OSCAR_ROUTE)
    {
        EstablishLayer("OSCAR_ROUTE_POINT", wkbPoint, TranslateOscarRoutePoint,
                       NRT_POINTREC, nullptr, "POINT_ID", OFTInteger, 6, 0,
                       "GEOM_ID", OFTInteger, 6, 0, "FEAT_CODE", OFTString, 4,
                       0, "OSODR", OFTString, 13, 0, "JUNCTION_NAME", OFTString,
                       0, 0, "SETTLE_NAME", OFTString, 0, 0, "NUM_PARENTS",
                       OFTInteger, 2, 0, "PARENT_OSODR", OFTStringList, 13, 0,
                       "ROUNDABOUT", OFTString, 1, 0, NULL);

        EstablishLayer("OSCAR_ROUTE_LINE", wkbLineString,
                       TranslateOscarRouteLine, NRT_LINEREC, nullptr, "LINE_ID",
                       OFTInteger, 6, 0, "GEOM_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "OSODR", OFTString, 13, 0,
                       "PROPER_NAME", OFTString, 0, 0, "LINE_LENGTH",
                       OFTInteger, 5, 0, "ROAD_NUM", OFTString, 0, 0,
                       "TRUNK_ROAD", OFTString, 1, 0, "NUM_PARENTS", OFTInteger,
                       2, 0, "PARENT_OSODR", OFTStringList, 13, 0, NULL);

        EstablishLayer("OSCAR_ROUTE_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, NULL);

        EstablishLayer("OSCAR_COMMENT", wkbNone, TranslateOscarComment,
                       NRT_COMMENT, nullptr, "RECORD_TYPE", OFTInteger, 2, 0,
                       "RECORD_ID", OFTString, 13, 0, "CHANGE_TYPE", OFTString,
                       1, 0, NULL);
    }
    else if (GetProductId() == NPC_OSCAR_NETWORK)
    {
        EstablishLayer("OSCAR_NETWORK_POINT", wkbPoint,
                       TranslateOscarNetworkPoint, NRT_POINTREC, nullptr,
                       "POINT_ID", OFTInteger, 6, 0, "GEOM_ID", OFTInteger, 6,
                       0, "FEAT_CODE", OFTString, 4, 0, "OSODR", OFTString, 13,
                       0, "JUNCTION_NAME", OFTString, 0, 0, "SETTLE_NAME",
                       OFTString, 0, 0, "ROUNDABOUT", OFTString, 1, 0, NULL);

        EstablishLayer("OSCAR_NETWORK_LINE", wkbLineString,
                       TranslateOscarNetworkLine, NRT_LINEREC, nullptr,
                       "LINE_ID", OFTInteger, 6, 0, "GEOM_ID", OFTInteger, 6, 0,
                       "FEAT_CODE", OFTString, 4, 0, "OSODR", OFTString, 13, 0,
                       "PROPER_NAME", OFTString, 0, 0, "LINE_LENGTH",
                       OFTInteger, 5, 0, "ROAD_NUM", OFTString, 0, 0, NULL);

        EstablishLayer("OSCAR_NETWORK_NODE", wkbNone, TranslateStrategiNode,
                       NRT_NODEREC, nullptr, "NODE_ID", OFTInteger, 6, 0,
                       "GEOM_ID_OF_POINT", OFTInteger, 6, 0, "NUM_LINKS",
                       OFTInteger, 4, 0, "DIR", OFTIntegerList, 1, 0,
                       "GEOM_ID_OF_LINK", OFTIntegerList, 6, 0, "LEVEL",
                       OFTIntegerList, 1, 0, NULL);

        EstablishLayer("OSCAR_COMMENT", wkbNone, TranslateOscarComment,
                       NRT_COMMENT, nullptr, "RECORD_TYPE", OFTInteger, 2, 0,
                       "RECORD_ID", OFTString, 13, 0, "CHANGE_TYPE", OFTString,
                       1, 0, NULL);
    }
    else if (GetProductId() == NPC_ADDRESS_POINT)
    {
        EstablishLayer(
            "ADDRESS_POINT", wkbPoint, TranslateAddressPoint, NRT_POINTREC,
            nullptr, "POINT_ID", OFTInteger, 6, 0, "OSAPR", OFTString, 18, 0,
            "ORGANISATION_NAME", OFTString, 0, 0, "DEPARTMENT_NAME", OFTString,
            0, 0, "PO_BOX", OFTString, 6, 0, "SUBBUILDING_NAME", OFTString, 0,
            0, "BUILDING_NAME", OFTString, 0, 0, "BUILDING_NUMBER", OFTInteger,
            4, 0, "DEPENDENT_THOROUGHFARE_NAME", OFTString, 0, 0,
            "THOROUGHFARE_NAME", OFTString, 0, 0,
            "DOUBLE_DEPENDENT_LOCALITY_NAME", OFTString, 0, 0,
            "DEPENDENT_LOCALITY_NAME", OFTString, 0, 0, "POST_TOWN_NAME",
            OFTString, 0, 0, "COUNTY_NAME", OFTString, 0, 0, "POSTCODE",
            OFTString, 7, 0, "STATUS_FLAG", OFTString, 4, 0, "RM_VERSION_DATE",
            OFTString, 8, 0, "CHG_TYPE", OFTString, 1, 0, "CHG_DATE", OFTString,
            6, 0, NULL);
    }
    else if (GetProductId() == NPC_CODE_POINT)
    {
        EstablishLayer(
            "CODE_POINT", wkbPoint, TranslateCodePoint, NRT_POINTREC, nullptr,
            "POINT_ID", OFTInteger, 6, 0, "UNIT_POSTCODE", OFTString, 7, 0,
            "POSITIONAL_QUALITY", OFTInteger, 1, 0, "PO_BOX_INDICATOR",
            OFTString, 1, 0, "TOTAL_DELIVERY_POINTS", OFTInteger, 3, 0,
            "DELIVERY_POINTS", OFTInteger, 3, 0, "DOMESTIC_DELIVERY_POINTS",
            OFTInteger, 3, 0, "NONDOMESTIC_DELIVERY_POINTS", OFTInteger, 3, 0,
            "POBOX_DELIVERY_POINTS", OFTInteger, 3, 0,
            "MATCHED_ADDRESS_PREMISES", OFTInteger, 3, 0,
            "UNMATCHED_DELIVERY_POINTS", OFTInteger, 3, 0, "RM_VERSION_DATA",
            OFTString, 8, 0, NULL);
    }
    else if (GetProductId() == NPC_CODE_POINT_PLUS)
    {
        EstablishLayer(
            "CODE_POINT_PLUS", wkbPoint, TranslateCodePoint, NRT_POINTREC,
            nullptr, "POINT_ID", OFTInteger, 6, 0, "UNIT_POSTCODE", OFTString,
            7, 0, "POSITIONAL_QUALITY", OFTInteger, 1, 0, "PO_BOX_INDICATOR",
            OFTString, 1, 0, "TOTAL_DELIVERY_POINTS", OFTInteger, 3, 0,
            "DELIVERY_POINTS", OFTInteger, 3, 0, "DOMESTIC_DELIVERY_POINTS",
            OFTInteger, 3, 0, "NONDOMESTIC_DELIVERY_POINTS", OFTInteger, 3, 0,
            "POBOX_DELIVERY_POINTS", OFTInteger, 3, 0,
            "MATCHED_ADDRESS_PREMISES", OFTInteger, 3, 0,
            "UNMATCHED_DELIVERY_POINTS", OFTInteger, 3, 0, "RM_VERSION_DATA",
            OFTString, 8, 0, "NHS_REGIONAL_HEALTH_AUTHORITY", OFTString, 3, 0,
            "NHS_HEALTH_AUTHORITY", OFTString, 3, 0, "ADMIN_COUNTY", OFTString,
            2, 0, "ADMIN_DISTRICT", OFTString, 2, 0, "ADMIN_WARD", OFTString, 2,
            0, NULL);
    }
    else  // generic case
    {
        CPLAssert(GetProductId() == NPC_UNKNOWN);

        poDS->WorkupGeneric(this);
    }
}
