/*
 * Copyright (c) 2010 Stan Coleby (scoleby@intelisum.com)
 * Copyright (c) 2020 PTC Inc.
 * Copyright (c) 2022 Andy Maloney <asmaloney@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#include "ReaderImpl.h"
#include "Common.h"
#include "StringFunctions.h"

namespace e57
{
   /*!
   @brief Reads the data out of a given image node

   @param [in] image 1 of 3 projects or the visual
   @param [in] imageType identifies the image format desired.
   @param [out] pBuffer pointer the buffer
   @param [out] start position in the block to start reading
   @param [out] count size of desired chunk or buffer size

   @return number of bytes read
   */
   size_t _readImage2DNode( const StructureNode &image, Image2DType imageType, uint8_t *pBuffer,
                            int64_t start, size_t count )
   {
      size_t transferred = 0;

      switch ( imageType )
      {
         case ImageNone:
            return 0;

         case ImageJPEG:
            if ( image.isDefined( "jpegImage" ) )
            {
               BlobNode jpegImage( image.get( "jpegImage" ) );
               jpegImage.read( pBuffer, start, count );
               transferred = count;
            }
            break;

         case ImagePNG:
            if ( image.isDefined( "pngImage" ) )
            {
               BlobNode pngImage( image.get( "pngImage" ) );
               pngImage.read( pBuffer, start, count );
               transferred = count;
            }
            break;

         case ImageMaskPNG:
            if ( image.isDefined( "imageMask" ) )
            {
               BlobNode imageMask( image.get( "imageMask" ) );
               imageMask.read( pBuffer, start, count );
               transferred = count;
            }
            break;
      }

      return transferred;
   }

   /*!
   @brief This function reads one of the image blobs

   @param [in] image 1 of 3 projects or the visual
   @param [out] imageType identifies the image format desired.
   @param [out] imageWidth The image width (in pixels).
   @param [out] imageHeight The image height (in pixels).
   @param [out] imageSize This is the total number of bytes for the image blob.
   @param [out] imageMaskType This is E57_PNG_IMAGE_MASK if "imageMask" is defined in the projection

   @return Returns true if successful
   */
   static bool _getImage2DNodeSizes( const StructureNode &image, Image2DType &imageType,
                                     int64_t &imageWidth, int64_t &imageHeight, int64_t &imageSize,
                                     Image2DType &imageMaskType )
   {
      imageWidth = 0;
      imageHeight = 0;
      imageSize = 0;
      imageType = ImageNone;
      imageMaskType = ImageNone;

      if ( image.isDefined( "imageWidth" ) )
      {
         imageWidth = IntegerNode( image.get( "imageWidth" ) ).value();
      }
      else
      {
         return false;
      }

      if ( image.isDefined( "imageHeight" ) )
      {
         imageHeight = IntegerNode( image.get( "imageHeight" ) ).value();
      }
      else
      {
         return false;
      }

      if ( image.isDefined( "jpegImage" ) )
      {
         imageSize = BlobNode( image.get( "jpegImage" ) ).byteCount();
         imageType = ImageJPEG;
      }
      else if ( image.isDefined( "pngImage" ) )
      {
         imageSize = BlobNode( image.get( "pngImage" ) ).byteCount();
         imageType = ImagePNG;
      }

      if ( image.isDefined( "imageMask" ) )
      {
         if ( imageType == ImageNone )
         {
            imageSize = BlobNode( image.get( "imageMask" ) ).byteCount();
            imageType = ImageMaskPNG;
         }

         imageMaskType = ImageMaskPNG;
      }

      return true;
   }

   /// Possibly get min/max from the colour node itself instead of the colorLimits.
   void _readColourRanges( const std::string &protoName, const StructureNode &proto,
                           double &colourMin, double &colourMax )
   {
      // IF the colorLimits are not set
      // AND our colour node is
      // THEN get our min/max from the colour node itself.
      if ( ( colourMax == 0.0 ) && proto.isDefined( protoName ) )
      {
         const auto colourProto = proto.get( protoName );

         if ( colourProto.type() == TypeInteger )
         {
            const IntegerNode integerColour( colourProto );

            colourMin = static_cast<uint16_t>( integerColour.minimum() );
            colourMax = static_cast<uint16_t>( integerColour.maximum() );
         }
         else if ( colourProto.type() == TypeScaledInteger )
         {
            const ScaledIntegerNode scaledColour( colourProto );
            const double scale = scaledColour.scale();
            const double offset = scaledColour.offset();
            const int64_t minimum = scaledColour.minimum();
            const int64_t maximum = scaledColour.maximum();

            colourMin = static_cast<uint16_t>( minimum ) * scale + offset;
            colourMax = static_cast<uint16_t>( maximum ) * scale + offset;
         }
         else if ( colourProto.type() == TypeFloat )
         {
            const FloatNode floatColour( colourProto );

            colourMin = static_cast<uint16_t>( floatColour.minimum() );
            colourMax = static_cast<uint16_t>( floatColour.maximum() );
         }
      }
   }

   ReaderImpl::ReaderImpl( const ustring &filePath, const ReaderOptions &options ) :
      imf_( filePath, "r", options.checksumPolicy ), root_( imf_.root() ),
      data3D_( root_.isDefined( "/data3D" ) ? root_.get( "/data3D" ) : VectorNode( imf_ ) ),
      images2D_( root_.isDefined( "/images2D" ) ? root_.get( "/images2D" ) : VectorNode( imf_ ) )
   {
   }

   ReaderImpl::~ReaderImpl()
   {
      if ( IsOpen() )
      {
         Close();
      }
   }

   bool ReaderImpl::IsOpen() const
   {
      return imf_.isOpen();
   }

   bool ReaderImpl::Close()
   {
      if ( !IsOpen() )
      {
         return false;
      }

      imf_.close();
      return true;
   }

   // Returns the file header information in fileHeader
   bool ReaderImpl::GetE57Root( E57Root &fileHeader ) const
   {
      if ( !IsOpen() )
      {
         return false;
      }

      fileHeader = {};

      fileHeader.formatName = StringNode( root_.get( "formatName" ) ).value();
      fileHeader.versionMajor =
         static_cast<uint32_t>( IntegerNode( root_.get( "versionMajor" ) ).value() );
      fileHeader.versionMinor =
         static_cast<uint32_t>( IntegerNode( root_.get( "versionMinor" ) ).value() );
      fileHeader.guid = StringNode( root_.get( "guid" ) ).value();
      if ( root_.isDefined( "e57LibraryVersion" ) )
      {
         fileHeader.e57LibraryVersion = StringNode( root_.get( "e57LibraryVersion" ) ).value();
      }

      if ( root_.isDefined( "coordinateMetadata" ) )
      {
         fileHeader.coordinateMetadata = StringNode( root_.get( "coordinateMetadata" ) ).value();
      }

      if ( root_.isDefined( "creationDateTime" ) )
      {
         const StructureNode creationDateTime( root_.get( "creationDateTime" ) );

         fileHeader.creationDateTime.dateTimeValue =
            FloatNode( creationDateTime.get( "dateTimeValue" ) ).value();

         if ( creationDateTime.isDefined( "isAtomicClockReferenced" ) )
         {
            fileHeader.creationDateTime.isAtomicClockReferenced = static_cast<int32_t>(
               IntegerNode( creationDateTime.get( "isAtomicClockReferenced" ) ).value() );
         }
      }

      fileHeader.data3DSize = data3D_.childCount();
      fileHeader.images2DSize = images2D_.childCount();

      return true;
   }

   int64_t ReaderImpl::GetImage2DCount() const
   {
      return images2D_.childCount();
   }

   // Returns the Image2Ds header and positions the cursor
   bool ReaderImpl::ReadImage2D( int64_t imageIndex, Image2D &image2DHeader ) const
   {
      if ( !IsOpen() )
      {
         return false;
      }
      if ( ( imageIndex < 0 ) || ( imageIndex >= images2D_.childCount() ) )
      {
         return false;
      }

      image2DHeader = {};

      const StructureNode image( images2D_.get( imageIndex ) );

      image2DHeader.guid = StringNode( image.get( "guid" ) ).value();

      if ( image.isDefined( "name" ) )
      {
         image2DHeader.name = StringNode( image.get( "name" ) ).value();
      }

      if ( image.isDefined( "description" ) )
      {
         image2DHeader.description = StringNode( image.get( "description" ) ).value();
      }

      if ( image.isDefined( "sensorVendor" ) )
      {
         image2DHeader.sensorVendor = StringNode( image.get( "sensorVendor" ) ).value();
      }
      if ( image.isDefined( "sensorModel" ) )
      {
         image2DHeader.sensorModel = StringNode( image.get( "sensorModel" ) ).value();
      }
      if ( image.isDefined( "sensorSerialNumber" ) )
      {
         image2DHeader.sensorSerialNumber = StringNode( image.get( "sensorSerialNumber" ) ).value();
      }

      if ( image.isDefined( "associatedData3DGuid" ) )
      {
         image2DHeader.associatedData3DGuid =
            StringNode( image.get( "associatedData3DGuid" ) ).value();
      }

      if ( image.isDefined( "acquisitionDateTime" ) )
      {
         const StructureNode acquisitionDateTime( image.get( "acquisitionDateTime" ) );

         image2DHeader.acquisitionDateTime.dateTimeValue =
            FloatNode( acquisitionDateTime.get( "dateTimeValue" ) ).value();

         if ( acquisitionDateTime.isDefined( "isAtomicClockReferenced" ) )
         {
            image2DHeader.acquisitionDateTime.isAtomicClockReferenced = static_cast<int32_t>(
               IntegerNode( acquisitionDateTime.get( "isAtomicClockReferenced" ) ).value() );
         }
      }

      // Get pose structure for scan.
      if ( image.isDefined( "pose" ) )
      {
         const StructureNode pose( image.get( "pose" ) );

         if ( pose.isDefined( "rotation" ) )
         {
            const StructureNode rotation( pose.get( "rotation" ) );

            image2DHeader.pose.rotation.w = FloatNode( rotation.get( "w" ) ).value();
            image2DHeader.pose.rotation.x = FloatNode( rotation.get( "x" ) ).value();
            image2DHeader.pose.rotation.y = FloatNode( rotation.get( "y" ) ).value();
            image2DHeader.pose.rotation.z = FloatNode( rotation.get( "z" ) ).value();
         }
         if ( pose.isDefined( "translation" ) )
         {
            const StructureNode translation( pose.get( "translation" ) );

            image2DHeader.pose.translation.x = FloatNode( translation.get( "x" ) ).value();
            image2DHeader.pose.translation.y = FloatNode( translation.get( "y" ) ).value();
            image2DHeader.pose.translation.z = FloatNode( translation.get( "z" ) ).value();
         }
      }

      if ( image.isDefined( "visualReferenceRepresentation" ) )
      {
         const StructureNode visualReferenceRepresentation(
            image.get( "visualReferenceRepresentation" ) );

         if ( visualReferenceRepresentation.isDefined( "jpegImage" ) )
         {
            image2DHeader.visualReferenceRepresentation.jpegImageSize =
               BlobNode( visualReferenceRepresentation.get( "jpegImage" ) ).byteCount();
         }
         if ( visualReferenceRepresentation.isDefined( "pngImage" ) )
         {
            image2DHeader.visualReferenceRepresentation.pngImageSize =
               BlobNode( visualReferenceRepresentation.get( "pngImage" ) ).byteCount();
         }
         if ( visualReferenceRepresentation.isDefined( "imageMask" ) )
         {
            image2DHeader.visualReferenceRepresentation.imageMaskSize =
               BlobNode( visualReferenceRepresentation.get( "imageMask" ) ).byteCount();
         }

         image2DHeader.visualReferenceRepresentation.imageHeight = static_cast<int32_t>(
            IntegerNode( visualReferenceRepresentation.get( "imageHeight" ) ).value() );
         image2DHeader.visualReferenceRepresentation.imageWidth = static_cast<int32_t>(
            IntegerNode( visualReferenceRepresentation.get( "imageWidth" ) ).value() );
      }

      if ( image.isDefined( "pinholeRepresentation" ) )
      {
         const StructureNode pinholeRepresentation( image.get( "pinholeRepresentation" ) );

         if ( pinholeRepresentation.isDefined( "jpegImage" ) )
         {
            image2DHeader.pinholeRepresentation.jpegImageSize =
               BlobNode( pinholeRepresentation.get( "jpegImage" ) ).byteCount();
         }
         if ( pinholeRepresentation.isDefined( "pngImage" ) )
         {
            image2DHeader.pinholeRepresentation.pngImageSize =
               BlobNode( pinholeRepresentation.get( "pngImage" ) ).byteCount();
         }
         if ( pinholeRepresentation.isDefined( "imageMask" ) )
         {
            image2DHeader.pinholeRepresentation.imageMaskSize =
               BlobNode( pinholeRepresentation.get( "imageMask" ) ).byteCount();
         }

         image2DHeader.pinholeRepresentation.focalLength =
            FloatNode( pinholeRepresentation.get( "focalLength" ) ).value();
         image2DHeader.pinholeRepresentation.imageHeight = static_cast<int32_t>(
            IntegerNode( pinholeRepresentation.get( "imageHeight" ) ).value() );
         image2DHeader.pinholeRepresentation.imageWidth = static_cast<int32_t>(
            IntegerNode( pinholeRepresentation.get( "imageWidth" ) ).value() );

         image2DHeader.pinholeRepresentation.pixelHeight =
            FloatNode( pinholeRepresentation.get( "pixelHeight" ) ).value();
         image2DHeader.pinholeRepresentation.pixelWidth =
            FloatNode( pinholeRepresentation.get( "pixelWidth" ) ).value();
         image2DHeader.pinholeRepresentation.principalPointX =
            FloatNode( pinholeRepresentation.get( "principalPointX" ) ).value();
         image2DHeader.pinholeRepresentation.principalPointY =
            FloatNode( pinholeRepresentation.get( "principalPointY" ) ).value();
      }
      else if ( image.isDefined( "sphericalRepresentation" ) )
      {
         const StructureNode sphericalRepresentation( image.get( "sphericalRepresentation" ) );

         if ( sphericalRepresentation.isDefined( "jpegImage" ) )
         {
            image2DHeader.sphericalRepresentation.jpegImageSize =
               BlobNode( sphericalRepresentation.get( "jpegImage" ) ).byteCount();
         }
         if ( sphericalRepresentation.isDefined( "pngImage" ) )
         {
            image2DHeader.sphericalRepresentation.pngImageSize =
               BlobNode( sphericalRepresentation.get( "pngImage" ) ).byteCount();
         }
         if ( sphericalRepresentation.isDefined( "imageMask" ) )
         {
            image2DHeader.sphericalRepresentation.imageMaskSize =
               BlobNode( sphericalRepresentation.get( "imageMask" ) ).byteCount();
         }

         image2DHeader.sphericalRepresentation.imageHeight = static_cast<int32_t>(
            IntegerNode( sphericalRepresentation.get( "imageHeight" ) ).value() );
         image2DHeader.sphericalRepresentation.imageWidth = static_cast<int32_t>(
            IntegerNode( sphericalRepresentation.get( "imageWidth" ) ).value() );

         image2DHeader.sphericalRepresentation.pixelHeight =
            FloatNode( sphericalRepresentation.get( "pixelHeight" ) ).value();
         image2DHeader.sphericalRepresentation.pixelWidth =
            FloatNode( sphericalRepresentation.get( "pixelWidth" ) ).value();
      }
      else if ( image.isDefined( "cylindricalRepresentation" ) )
      {
         const StructureNode cylindricalRepresentation( image.get( "cylindricalRepresentation" ) );

         if ( cylindricalRepresentation.isDefined( "jpegImage" ) )
         {
            image2DHeader.cylindricalRepresentation.jpegImageSize =
               BlobNode( cylindricalRepresentation.get( "jpegImage" ) ).byteCount();
         }
         if ( cylindricalRepresentation.isDefined( "pngImage" ) )
         {
            image2DHeader.cylindricalRepresentation.pngImageSize =
               BlobNode( cylindricalRepresentation.get( "pngImage" ) ).byteCount();
         }
         if ( cylindricalRepresentation.isDefined( "imageMask" ) )
         {
            image2DHeader.cylindricalRepresentation.imageMaskSize =
               BlobNode( cylindricalRepresentation.get( "imageMask" ) ).byteCount();
         }

         image2DHeader.cylindricalRepresentation.imageHeight = static_cast<int32_t>(
            IntegerNode( cylindricalRepresentation.get( "imageHeight" ) ).value() );
         image2DHeader.cylindricalRepresentation.imageWidth = static_cast<int32_t>(
            IntegerNode( cylindricalRepresentation.get( "imageWidth" ) ).value() );

         image2DHeader.cylindricalRepresentation.pixelHeight =
            FloatNode( cylindricalRepresentation.get( "pixelHeight" ) ).value();
         image2DHeader.cylindricalRepresentation.pixelWidth =
            FloatNode( cylindricalRepresentation.get( "pixelWidth" ) ).value();
         image2DHeader.cylindricalRepresentation.principalPointY =
            FloatNode( cylindricalRepresentation.get( "principalPointY" ) ).value();
         image2DHeader.cylindricalRepresentation.radius =
            FloatNode( cylindricalRepresentation.get( "radius" ) ).value();
      }

      return true;
   }

   // Returns the image sizes
   bool ReaderImpl::GetImage2DSizes( int64_t imageIndex, Image2DProjection &imageProjection,
                                     Image2DType &imageType, int64_t &imageWidth,
                                     int64_t &imageHeight, int64_t &imageSize,
                                     Image2DType &imageMaskType,
                                     Image2DType &imageVisualType ) const
   {
      if ( ( imageIndex < 0 ) || ( imageIndex >= images2D_.childCount() ) )
      {
         return false;
      }

      imageProjection = ProjectionNone;
      imageType = ImageNone;
      imageMaskType = ImageNone;
      imageVisualType = ImageNone;

      const StructureNode image( images2D_.get( imageIndex ) );

      if ( image.isDefined( "visualReferenceRepresentation" ) )
      {
         const StructureNode visualReferenceRepresentation(
            image.get( "visualReferenceRepresentation" ) );

         bool ret = _getImage2DNodeSizes( visualReferenceRepresentation, imageType, imageWidth,
                                          imageHeight, imageSize, imageMaskType );
         imageProjection = ProjectionVisual;
         imageVisualType = imageType;

         return ret;
      }

      if ( image.isDefined( "pinholeRepresentation" ) )
      {
         const StructureNode pinholeRepresentation( image.get( "pinholeRepresentation" ) );

         imageProjection = ProjectionPinhole;

         return _getImage2DNodeSizes( pinholeRepresentation, imageType, imageWidth, imageHeight,
                                      imageSize, imageMaskType );
      }

      if ( image.isDefined( "sphericalRepresentation" ) )
      {
         const StructureNode sphericalRepresentation( image.get( "sphericalRepresentation" ) );

         imageProjection = ProjectionSpherical;

         return _getImage2DNodeSizes( sphericalRepresentation, imageType, imageWidth, imageHeight,
                                      imageSize, imageMaskType );
      }

      if ( image.isDefined( "cylindricalRepresentation" ) )
      {
         const StructureNode cylindricalRepresentation( image.get( "cylindricalRepresentation" ) );

         imageProjection = ProjectionCylindrical;

         return _getImage2DNodeSizes( cylindricalRepresentation, imageType, imageWidth, imageHeight,
                                      imageSize, imageMaskType );
      }

      return false;
   }

   // Reads the image data block
   size_t ReaderImpl::ReadImage2DData( int64_t imageIndex, Image2DProjection imageProjection,
                                       Image2DType imageType, uint8_t *pBuffer, int64_t start,
                                       size_t count ) const
   {
      if ( ( imageIndex < 0 ) || ( imageIndex >= images2D_.childCount() ) )
      {
         return 0;
      }

      const StructureNode image( images2D_.get( imageIndex ) );

      switch ( imageProjection )
      {
         case ProjectionNone:
            return 0;

         case ProjectionVisual:
            if ( image.isDefined( "visualReferenceRepresentation" ) )
            {
               const StructureNode visualReferenceRepresentation(
                  image.get( "visualReferenceRepresentation" ) );

               return _readImage2DNode( visualReferenceRepresentation, imageType, pBuffer, start,
                                        count );
            }
            break;

         case ProjectionPinhole:
            if ( image.isDefined( "pinholeRepresentation" ) )
            {
               const StructureNode pinholeRepresentation( image.get( "pinholeRepresentation" ) );

               return _readImage2DNode( pinholeRepresentation, imageType, pBuffer, start, count );
            }
            break;

         case ProjectionSpherical:
            if ( image.isDefined( "sphericalRepresentation" ) )
            {
               const StructureNode sphericalRepresentation(
                  image.get( "sphericalRepresentation" ) );

               return _readImage2DNode( sphericalRepresentation, imageType, pBuffer, start, count );
            }
            break;

         case ProjectionCylindrical:
            if ( image.isDefined( "cylindricalRepresentation" ) )
            {
               const StructureNode cylindricalRepresentation(
                  image.get( "cylindricalRepresentation" ) );

               return _readImage2DNode( cylindricalRepresentation, imageType, pBuffer, start,
                                        count );
            }
            break;
      }

      return 0;
   }

   bool ReaderImpl::ReadData3D( int64_t dataIndex, Data3D &data3DHeader ) const
   {
      if ( !IsOpen() || ( dataIndex < 0 ) || ( dataIndex >= data3D_.childCount() ) )
      {
         return false;
      }

      data3DHeader = {};

      const StructureNode scan( data3D_.get( dataIndex ) );
      CompressedVectorNode points( scan.get( "points" ) );
      const StructureNode proto( points.prototype() );

      data3DHeader.guid = StringNode( scan.get( "guid" ) ).value();

#ifdef E57_32_BIT
      // If we exceed the size_t max, only process the max (4,294,967,295 points).
      if ( points.childCount() > static_cast<int64_t>( std::numeric_limits<size_t>::max() ) )
      {
         data3DHeader.pointCount = std::numeric_limits<size_t>::max();

         std::cout << "Warning (32-bit): Point count (" << points.childCount()
                   << ") exceeds storage capacity (" << std::numeric_limits<size_t>::max()
                   << "). Dropping " << points.childCount() - std::numeric_limits<size_t>::max()
                   << " points from scan." << std::endl;
      }
      else
      {
         data3DHeader.pointCount = static_cast<size_t>( points.childCount() );
      }
#else
      data3DHeader.pointCount = points.childCount();
#endif

      if ( scan.isDefined( "name" ) )
      {
         data3DHeader.name = StringNode( scan.get( "name" ) ).value();
      }
      if ( scan.isDefined( "description" ) )
      {
         data3DHeader.description = StringNode( scan.get( "description" ) ).value();
      }

      if ( scan.isDefined( "originalGuids" ) )
      {
         const VectorNode originalGuids( scan.get( "originalGuids" ) );

         if ( originalGuids.childCount() > 0 )
         {
            data3DHeader.originalGuids.clear();

            for ( int64_t i = 0; i < originalGuids.childCount(); ++i )
            {
               const ustring str = StringNode( originalGuids.get( i ) ).value();

               data3DHeader.originalGuids.push_back( str );
            }
         }
      }

      // Get various sensor and version strings from scan.
      if ( scan.isDefined( "sensorVendor" ) )
      {
         data3DHeader.sensorVendor = StringNode( scan.get( "sensorVendor" ) ).value();
      }
      if ( scan.isDefined( "sensorModel" ) )
      {
         data3DHeader.sensorModel = StringNode( scan.get( "sensorModel" ) ).value();
      }
      if ( scan.isDefined( "sensorSerialNumber" ) )
      {
         data3DHeader.sensorSerialNumber = StringNode( scan.get( "sensorSerialNumber" ) ).value();
      }
      if ( scan.isDefined( "sensorHardwareVersion" ) )
      {
         data3DHeader.sensorHardwareVersion =
            StringNode( scan.get( "sensorHardwareVersion" ) ).value();
      }
      if ( scan.isDefined( "sensorSoftwareVersion" ) )
      {
         data3DHeader.sensorSoftwareVersion =
            StringNode( scan.get( "sensorSoftwareVersion" ) ).value();
      }
      if ( scan.isDefined( "sensorFirmwareVersion" ) )
      {
         data3DHeader.sensorFirmwareVersion =
            StringNode( scan.get( "sensorFirmwareVersion" ) ).value();
      }

      // Get temp/humidity from scan.
      if ( scan.isDefined( "temperature" ) )
      {
         data3DHeader.temperature =
            static_cast<float>( FloatNode( scan.get( "temperature" ) ).value() );
      }
      if ( scan.isDefined( "relativeHumidity" ) )
      {
         data3DHeader.relativeHumidity =
            static_cast<float>( FloatNode( scan.get( "relativeHumidity" ) ).value() );
      }
      if ( scan.isDefined( "atmosphericPressure" ) )
      {
         data3DHeader.atmosphericPressure =
            static_cast<float>( FloatNode( scan.get( "atmosphericPressure" ) ).value() );
      }

      if ( scan.isDefined( "indexBounds" ) )
      {
         const StructureNode ibox( scan.get( "indexBounds" ) );

         if ( ibox.isDefined( "rowMaximum" ) )
         {
            data3DHeader.indexBounds.rowMinimum = IntegerNode( ibox.get( "rowMinimum" ) ).value();
            data3DHeader.indexBounds.rowMaximum = IntegerNode( ibox.get( "rowMaximum" ) ).value();
         }
         if ( ibox.isDefined( "columnMaximum" ) )
         {
            data3DHeader.indexBounds.columnMinimum =
               IntegerNode( ibox.get( "columnMinimum" ) ).value();
            data3DHeader.indexBounds.columnMaximum =
               IntegerNode( ibox.get( "columnMaximum" ) ).value();
         }
         if ( ibox.isDefined( "returnMaximum" ) )
         {
            data3DHeader.indexBounds.returnMinimum =
               IntegerNode( ibox.get( "returnMinimum" ) ).value();
            data3DHeader.indexBounds.returnMaximum =
               IntegerNode( ibox.get( "returnMaximum" ) ).value();
         }
      }

      if ( scan.isDefined( "pointGroupingSchemes" ) )
      {
         const StructureNode pointGroupingSchemes( scan.get( "pointGroupingSchemes" ) );

         if ( pointGroupingSchemes.isDefined( "groupingByLine" ) )
         {
            const StructureNode groupingByLine( pointGroupingSchemes.get( "groupingByLine" ) );

            data3DHeader.pointGroupingSchemes.groupingByLine.idElementName =
               StringNode( groupingByLine.get( "idElementName" ) ).value();

            const CompressedVectorNode groups( groupingByLine.get( "groups" ) );
            const StructureNode lineGroupRecord( groups.prototype() );

            data3DHeader.pointGroupingSchemes.groupingByLine.groupsSize = groups.childCount();

            if ( lineGroupRecord.isDefined( "pointCount" ) )
            {
               data3DHeader.pointGroupingSchemes.groupingByLine.pointCountSize =
                  IntegerNode( lineGroupRecord.get( "pointCount" ) ).maximum();
            }
         }
      }

      // Get Cartesian bounding box from scan.
      if ( scan.isDefined( "cartesianBounds" ) )
      {
         const StructureNode bbox( scan.get( "cartesianBounds" ) );

         if ( bbox.get( "xMinimum" ).type() == TypeScaledInteger )
         {
            data3DHeader.cartesianBounds.xMinimum =
               ScaledIntegerNode( bbox.get( "xMinimum" ) ).scaledValue();
            data3DHeader.cartesianBounds.xMaximum =
               ScaledIntegerNode( bbox.get( "xMaximum" ) ).scaledValue();
            data3DHeader.cartesianBounds.yMinimum =
               ScaledIntegerNode( bbox.get( "yMinimum" ) ).scaledValue();
            data3DHeader.cartesianBounds.yMaximum =
               ScaledIntegerNode( bbox.get( "yMaximum" ) ).scaledValue();
            data3DHeader.cartesianBounds.zMinimum =
               ScaledIntegerNode( bbox.get( "zMinimum" ) ).scaledValue();
            data3DHeader.cartesianBounds.zMaximum =
               ScaledIntegerNode( bbox.get( "zMaximum" ) ).scaledValue();
         }
         else if ( bbox.get( "xMinimum" ).type() == TypeFloat )
         {
            data3DHeader.cartesianBounds.xMinimum = FloatNode( bbox.get( "xMinimum" ) ).value();
            data3DHeader.cartesianBounds.xMaximum = FloatNode( bbox.get( "xMaximum" ) ).value();
            data3DHeader.cartesianBounds.yMinimum = FloatNode( bbox.get( "yMinimum" ) ).value();
            data3DHeader.cartesianBounds.yMaximum = FloatNode( bbox.get( "yMaximum" ) ).value();
            data3DHeader.cartesianBounds.zMinimum = FloatNode( bbox.get( "zMinimum" ) ).value();
            data3DHeader.cartesianBounds.zMaximum = FloatNode( bbox.get( "zMaximum" ) ).value();
         }
      }

      if ( scan.isDefined( "sphericalBounds" ) )
      {
         const StructureNode sbox( scan.get( "sphericalBounds" ) );

         if ( sbox.get( "rangeMinimum" ).type() == TypeScaledInteger )
         {
            data3DHeader.sphericalBounds.rangeMinimum =
               ScaledIntegerNode( sbox.get( "rangeMinimum" ) ).scaledValue();
            data3DHeader.sphericalBounds.rangeMaximum =
               ScaledIntegerNode( sbox.get( "rangeMaximum" ) ).scaledValue();
         }
         else if ( sbox.get( "rangeMinimum" ).type() == TypeFloat )
         {
            data3DHeader.sphericalBounds.rangeMinimum =
               FloatNode( sbox.get( "rangeMinimum" ) ).value();
            data3DHeader.sphericalBounds.rangeMaximum =
               FloatNode( sbox.get( "rangeMaximum" ) ).value();
         }

         if ( sbox.get( "elevationMinimum" ).type() == TypeScaledInteger )
         {
            data3DHeader.sphericalBounds.elevationMinimum =
               ScaledIntegerNode( sbox.get( "elevationMinimum" ) ).scaledValue();
            data3DHeader.sphericalBounds.elevationMaximum =
               ScaledIntegerNode( sbox.get( "elevationMaximum" ) ).scaledValue();
         }
         else if ( sbox.get( "elevationMinimum" ).type() == TypeFloat )
         {
            data3DHeader.sphericalBounds.elevationMinimum =
               FloatNode( sbox.get( "elevationMinimum" ) ).value();
            data3DHeader.sphericalBounds.elevationMaximum =
               FloatNode( sbox.get( "elevationMaximum" ) ).value();
         }

         if ( sbox.get( "azimuthStart" ).type() == TypeScaledInteger )
         {
            data3DHeader.sphericalBounds.azimuthStart =
               ScaledIntegerNode( sbox.get( "azimuthStart" ) ).scaledValue();
            data3DHeader.sphericalBounds.azimuthEnd =
               ScaledIntegerNode( sbox.get( "azimuthEnd" ) ).scaledValue();
         }
         else if ( sbox.get( "azimuthStart" ).type() == TypeFloat )
         {
            data3DHeader.sphericalBounds.azimuthStart =
               FloatNode( sbox.get( "azimuthStart" ) ).value();
            data3DHeader.sphericalBounds.azimuthEnd = FloatNode( sbox.get( "azimuthEnd" ) ).value();
         }
      }

      // Get pose structure from scan.
      if ( scan.isDefined( "pose" ) )
      {
         const StructureNode pose( scan.get( "pose" ) );

         if ( pose.isDefined( "rotation" ) )
         {
            const StructureNode rotation( pose.get( "rotation" ) );

            data3DHeader.pose.rotation.w = FloatNode( rotation.get( "w" ) ).value();
            data3DHeader.pose.rotation.x = FloatNode( rotation.get( "x" ) ).value();
            data3DHeader.pose.rotation.y = FloatNode( rotation.get( "y" ) ).value();
            data3DHeader.pose.rotation.z = FloatNode( rotation.get( "z" ) ).value();
         }

         if ( pose.isDefined( "translation" ) )
         {
            const StructureNode translation( pose.get( "translation" ) );

            data3DHeader.pose.translation.x = FloatNode( translation.get( "x" ) ).value();
            data3DHeader.pose.translation.y = FloatNode( translation.get( "y" ) ).value();
            data3DHeader.pose.translation.z = FloatNode( translation.get( "z" ) ).value();
         }
      }

      // Get start/stop acquisition times from scan.
      if ( scan.isDefined( "acquisitionStart" ) )
      {
         const StructureNode acquisitionStart( scan.get( "acquisitionStart" ) );

         data3DHeader.acquisitionStart.dateTimeValue =
            FloatNode( acquisitionStart.get( "dateTimeValue" ) ).value();

         if ( acquisitionStart.isDefined( "isAtomicClockReferenced" ) )
         {
            data3DHeader.acquisitionStart.isAtomicClockReferenced = static_cast<int32_t>(
               IntegerNode( acquisitionStart.get( "isAtomicClockReferenced" ) ).value() );
         }
      }

      if ( scan.isDefined( "acquisitionEnd" ) )
      {
         const StructureNode acquisitionEnd( scan.get( "acquisitionEnd" ) );

         data3DHeader.acquisitionEnd.dateTimeValue =
            FloatNode( acquisitionEnd.get( "dateTimeValue" ) ).value();

         if ( acquisitionEnd.isDefined( "isAtomicClockReferenced" ) )
         {
            data3DHeader.acquisitionEnd.isAtomicClockReferenced = static_cast<int32_t>(
               IntegerNode( acquisitionEnd.get( "isAtomicClockReferenced" ) ).value() );
         }
      }

      // Get a prototype of datatypes that are stored in points record.
      data3DHeader.pointFields.cartesianXField = proto.isDefined( "cartesianX" );
      data3DHeader.pointFields.cartesianYField = proto.isDefined( "cartesianY" );
      data3DHeader.pointFields.cartesianZField = proto.isDefined( "cartesianZ" );
      data3DHeader.pointFields.cartesianInvalidStateField =
         proto.isDefined( "cartesianInvalidState" );

      data3DHeader.pointFields.pointRangeMinimum = 0.0;
      data3DHeader.pointFields.pointRangeMaximum = 0.0;

      if ( proto.isDefined( "cartesianX" ) )
      {
         const auto cartesianXProto = proto.get( "cartesianX" );

         switch ( cartesianXProto.type() )
         {
            case TypeInteger:
            {
               // Should be a warning that we don't handle this type, but we don't have a mechanism
               // for warnings.
               break;
            }

            case TypeScaledInteger:
            {
               const ScaledIntegerNode scaledCartesianX( cartesianXProto );
               const double scale = scaledCartesianX.scale();
               const double offset = scaledCartesianX.offset();
               const int64_t minimum = scaledCartesianX.minimum();
               const int64_t maximum = scaledCartesianX.maximum();

               data3DHeader.pointFields.pointRangeMinimum =
                  static_cast<double>( minimum ) * scale + offset;
               data3DHeader.pointFields.pointRangeMaximum =
                  static_cast<double>( maximum ) * scale + offset;

               data3DHeader.pointFields.pointRangeNodeType = NumericalNodeType::ScaledInteger;
               data3DHeader.pointFields.pointRangeScale = scale;

               break;
            }

            case TypeFloat:
            {
               const FloatNode floatCartesianX( cartesianXProto );

               data3DHeader.pointFields.pointRangeMinimum = floatCartesianX.minimum();
               data3DHeader.pointFields.pointRangeMaximum = floatCartesianX.maximum();

               if ( floatCartesianX.precision() == PrecisionSingle )
               {
                  data3DHeader.pointFields.pointRangeNodeType = NumericalNodeType::Float;
               }
               else
               {
                  data3DHeader.pointFields.pointRangeNodeType = NumericalNodeType::Double;
               }

               break;
            }

            default:
               throw E57_EXCEPTION2( ErrorInvalidNodeType,
                                     "invalid node type reading cartesianX field: " +
                                        toString( cartesianXProto.type() ) );
               break;
         }
      }
      else if ( proto.isDefined( "sphericalRange" ) )
      {
         const auto sphericalRangeProto = proto.get( "sphericalRange" );

         switch ( sphericalRangeProto.type() )
         {
            case TypeInteger:
            {
               // Should be a warning that we don't handle this type, but we don't have a mechanism
               // for warnings.
               break;
            }

            case TypeScaledInteger:
            {
               const ScaledIntegerNode scaledSphericalRange( sphericalRangeProto );
               const double scale = scaledSphericalRange.scale();
               const double offset = scaledSphericalRange.offset();
               const int64_t minimum = scaledSphericalRange.minimum();
               const int64_t maximum = scaledSphericalRange.maximum();

               data3DHeader.pointFields.pointRangeMinimum =
                  static_cast<double>( minimum ) * scale + offset;
               data3DHeader.pointFields.pointRangeMaximum =
                  static_cast<double>( maximum ) * scale + offset;

               data3DHeader.pointFields.pointRangeNodeType = NumericalNodeType::ScaledInteger;
               data3DHeader.pointFields.pointRangeScale = scale;

               break;
            }

            case TypeFloat:
            {
               const FloatNode floatSphericalRange( sphericalRangeProto );

               data3DHeader.pointFields.pointRangeMinimum = floatSphericalRange.minimum();
               data3DHeader.pointFields.pointRangeMaximum = floatSphericalRange.maximum();

               if ( floatSphericalRange.precision() == PrecisionSingle )
               {
                  data3DHeader.pointFields.pointRangeNodeType = NumericalNodeType::Float;
               }
               else
               {
                  data3DHeader.pointFields.pointRangeNodeType = NumericalNodeType::Double;
               }

               break;
            }

            default:
               throw E57_EXCEPTION2( ErrorInvalidNodeType,
                                     "invalid node type reading sphericalRange field: " +
                                        toString( sphericalRangeProto.type() ) );
               break;
         }
      }

      data3DHeader.pointFields.sphericalRangeField = proto.isDefined( "sphericalRange" );
      data3DHeader.pointFields.sphericalAzimuthField = proto.isDefined( "sphericalAzimuth" );
      data3DHeader.pointFields.sphericalElevationField = proto.isDefined( "sphericalElevation" );
      data3DHeader.pointFields.sphericalInvalidStateField =
         proto.isDefined( "sphericalInvalidState" );

      data3DHeader.pointFields.angleMinimum = 0.0;
      data3DHeader.pointFields.angleMaximum = 0.0;

      if ( proto.isDefined( "sphericalAzimuth" ) )
      {
         const auto sphericalAzimuthProto = proto.get( "sphericalAzimuth" );

         switch ( sphericalAzimuthProto.type() )
         {
            case TypeScaledInteger:
            {
               const ScaledIntegerNode scaledSphericalAzimuth( sphericalAzimuthProto );
               const double scale = scaledSphericalAzimuth.scale();
               const double offset = scaledSphericalAzimuth.offset();
               const int64_t minimum = scaledSphericalAzimuth.minimum();
               const int64_t maximum = scaledSphericalAzimuth.maximum();

               data3DHeader.pointFields.angleMinimum =
                  static_cast<double>( minimum ) * scale + offset;
               data3DHeader.pointFields.angleMaximum =
                  static_cast<double>( maximum ) * scale + offset;

               data3DHeader.pointFields.angleNodeType = NumericalNodeType::ScaledInteger;
               data3DHeader.pointFields.angleScale = scale;

               break;
            }

            case TypeFloat:
            {
               const FloatNode floatSphericalAzimuth( sphericalAzimuthProto );

               data3DHeader.pointFields.angleMinimum = floatSphericalAzimuth.minimum();
               data3DHeader.pointFields.angleMaximum = floatSphericalAzimuth.maximum();

               if ( floatSphericalAzimuth.precision() == PrecisionSingle )
               {
                  data3DHeader.pointFields.angleNodeType = NumericalNodeType::Float;
               }
               else
               {
                  data3DHeader.pointFields.angleNodeType = NumericalNodeType::Double;
               }

               break;
            }

            default:
               throw E57_EXCEPTION2( ErrorInvalidNodeType,
                                     "invalid node type reading sphericalAzimuth field: " +
                                        toString( sphericalAzimuthProto.type() ) );
               break;
         }
      }

      data3DHeader.pointFields.rowIndexField = proto.isDefined( "rowIndex" );
      data3DHeader.pointFields.columnIndexField = proto.isDefined( "columnIndex" );
      data3DHeader.pointFields.rowIndexMaximum = 0;
      data3DHeader.pointFields.columnIndexMaximum = 0;

      if ( proto.isDefined( "rowIndex" ) )
      {
         data3DHeader.pointFields.rowIndexMaximum =
            static_cast<uint32_t>( IntegerNode( proto.get( "rowIndex" ) ).maximum() );
      }

      if ( proto.isDefined( "columnIndex" ) )
      {
         data3DHeader.pointFields.columnIndexMaximum =
            static_cast<uint32_t>( IntegerNode( proto.get( "columnIndex" ) ).maximum() );
      }

      data3DHeader.pointFields.returnIndexField = proto.isDefined( "returnIndex" );
      data3DHeader.pointFields.returnCountField = proto.isDefined( "returnCount" );
      data3DHeader.pointFields.returnMaximum = 0;

      if ( proto.isDefined( "returnIndex" ) )
      {
         data3DHeader.pointFields.returnMaximum =
            static_cast<uint8_t>( IntegerNode( proto.get( "returnIndex" ) ).maximum() );
      }

      data3DHeader.pointFields.timeStampField = proto.isDefined( "timeStamp" );
      data3DHeader.pointFields.isTimeStampInvalidField = proto.isDefined( "isTimeStampInvalid" );
      data3DHeader.pointFields.timeMaximum = 0.0;
      data3DHeader.pointFields.timeMinimum = 0.0;

      if ( proto.isDefined( "timeStamp" ) )
      {
         const auto timeStampProto = proto.get( "timeStamp" );

         switch ( timeStampProto.type() )
         {
            case TypeInteger:
            {
               const IntegerNode integerTimeStamp( timeStampProto );

               data3DHeader.pointFields.timeMaximum =
                  static_cast<double>( integerTimeStamp.maximum() );
               data3DHeader.pointFields.timeMinimum =
                  static_cast<double>( integerTimeStamp.minimum() );

               data3DHeader.pointFields.timeNodeType = NumericalNodeType::Integer;

               break;
            }

            case TypeScaledInteger:
            {
               const ScaledIntegerNode scaledTimeStamp( timeStampProto );

               const double scale = scaledTimeStamp.scale();
               const double offset = scaledTimeStamp.offset();
               const int64_t minimum = scaledTimeStamp.minimum();
               const int64_t maximum = scaledTimeStamp.maximum();

               data3DHeader.pointFields.timeMinimum =
                  static_cast<double>( minimum ) * scale + offset;
               data3DHeader.pointFields.timeMaximum =
                  static_cast<double>( maximum ) * scale + offset;

               data3DHeader.pointFields.timeNodeType = NumericalNodeType::ScaledInteger;
               data3DHeader.pointFields.timeScale = scale;

               break;
            }

            case TypeFloat:
            {
               const FloatNode floatTimeStamp( timeStampProto );

               data3DHeader.pointFields.timeMinimum = floatTimeStamp.minimum();
               data3DHeader.pointFields.timeMaximum = floatTimeStamp.maximum();

               if ( floatTimeStamp.precision() == PrecisionSingle )
               {
                  data3DHeader.pointFields.timeNodeType = NumericalNodeType::Float;
               }
               else
               {
                  data3DHeader.pointFields.timeNodeType = NumericalNodeType::Double;
               }

               break;
            }

            default:
               throw E57_EXCEPTION2( ErrorInvalidNodeType,
                                     "invalid node type reading timeStamp field: " +
                                        toString( timeStampProto.type() ) );
               break;
         }
      }

      data3DHeader.pointFields.intensityField = proto.isDefined( "intensity" );
      data3DHeader.pointFields.isIntensityInvalidField = proto.isDefined( "isIntensityInvalid" );
      data3DHeader.intensityLimits.intensityMinimum = 0.0;
      data3DHeader.intensityLimits.intensityMaximum = 0.0;

      if ( scan.isDefined( "intensityLimits" ) )
      {
         const StructureNode intbox( scan.get( "intensityLimits" ) );
         const auto intensityMaximumProto = intbox.get( "intensityMaximum" );
         const auto intensityMinimumProto = intbox.get( "intensityMinimum" );

         if ( intensityMaximumProto.type() == TypeScaledInteger )
         {
            data3DHeader.intensityLimits.intensityMaximum =
               ScaledIntegerNode( intensityMaximumProto ).scaledValue();
            data3DHeader.intensityLimits.intensityMinimum =
               ScaledIntegerNode( intensityMinimumProto ).scaledValue();
         }
         else if ( intensityMaximumProto.type() == TypeFloat )
         {
            data3DHeader.intensityLimits.intensityMaximum =
               FloatNode( intensityMaximumProto ).value();
            data3DHeader.intensityLimits.intensityMinimum =
               FloatNode( intensityMinimumProto ).value();
         }
         else if ( intensityMaximumProto.type() == TypeInteger )
         {
            data3DHeader.intensityLimits.intensityMaximum =
               static_cast<double>( IntegerNode( intensityMaximumProto ).value() );
            data3DHeader.intensityLimits.intensityMinimum =
               static_cast<double>( IntegerNode( intensityMinimumProto ).value() );
         }
      }

      if ( proto.isDefined( "intensity" ) )
      {
         const auto intensityProto = proto.get( "intensity" );

         switch ( intensityProto.type() )
         {
            case TypeInteger:
            {
               const IntegerNode integerIntensity( intensityProto );

               if ( data3DHeader.intensityLimits.intensityMaximum == 0.0 )
               {
                  data3DHeader.intensityLimits.intensityMinimum =
                     static_cast<double>( integerIntensity.minimum() );
                  data3DHeader.intensityLimits.intensityMaximum =
                     static_cast<double>( integerIntensity.maximum() );
               }

               data3DHeader.pointFields.intensityNodeType = NumericalNodeType::Integer;

               break;
            }

            case TypeScaledInteger:
            {
               const ScaledIntegerNode scaledIntensity( intensityProto );
               double scale = scaledIntensity.scale();
               double offset = scaledIntensity.offset();

               if ( data3DHeader.intensityLimits.intensityMaximum == 0.0 )
               {
                  const int64_t minimum = scaledIntensity.minimum();
                  const int64_t maximum = scaledIntensity.maximum();

                  data3DHeader.intensityLimits.intensityMinimum =
                     static_cast<double>( minimum ) * scale + offset;
                  data3DHeader.intensityLimits.intensityMaximum =
                     static_cast<double>( maximum ) * scale + offset;
               }

               data3DHeader.pointFields.intensityNodeType = NumericalNodeType::ScaledInteger;
               data3DHeader.pointFields.intensityScale = scale;

               break;
            }

            case TypeFloat:
            {
               const FloatNode floatIntensity( intensityProto );

               data3DHeader.intensityLimits.intensityMinimum = floatIntensity.minimum();
               data3DHeader.intensityLimits.intensityMaximum = floatIntensity.maximum();

               if ( floatIntensity.precision() == PrecisionSingle )
               {
                  data3DHeader.pointFields.intensityNodeType = NumericalNodeType::Float;
               }
               else
               {
                  data3DHeader.pointFields.intensityNodeType = NumericalNodeType::Double;
               }

               break;
            }

            default:
               throw E57_EXCEPTION2( ErrorInvalidNodeType,
                                     "invalid node type reading intensity field: " +
                                        toString( intensityProto.type() ) );
               break;
         }
      }

      data3DHeader.pointFields.colorRedField = proto.isDefined( "colorRed" );
      data3DHeader.pointFields.colorGreenField = proto.isDefined( "colorGreen" );
      data3DHeader.pointFields.colorBlueField = proto.isDefined( "colorBlue" );
      data3DHeader.pointFields.isColorInvalidField = proto.isDefined( "isColorInvalid" );

      data3DHeader.colorLimits.colorRedMinimum = 0.0;
      data3DHeader.colorLimits.colorRedMaximum = 0.0;
      data3DHeader.colorLimits.colorGreenMinimum = 0.0;
      data3DHeader.colorLimits.colorGreenMaximum = 0.0;
      data3DHeader.colorLimits.colorBlueMinimum = 0.0;
      data3DHeader.colorLimits.colorBlueMaximum = 0.0;

      if ( scan.isDefined( "colorLimits" ) )
      {
         const StructureNode colorbox( scan.get( "colorLimits" ) );

         if ( colorbox.get( "colorRedMaximum" ).type() == TypeScaledInteger )
         {
            data3DHeader.colorLimits.colorRedMaximum =
               ScaledIntegerNode( colorbox.get( "colorRedMaximum" ) ).scaledValue();
            data3DHeader.colorLimits.colorRedMinimum =
               ScaledIntegerNode( colorbox.get( "colorRedMinimum" ) ).scaledValue();
            data3DHeader.colorLimits.colorGreenMaximum =
               ScaledIntegerNode( colorbox.get( "colorGreenMaximum" ) ).scaledValue();
            data3DHeader.colorLimits.colorGreenMinimum =
               ScaledIntegerNode( colorbox.get( "colorGreenMinimum" ) ).scaledValue();
            data3DHeader.colorLimits.colorBlueMaximum =
               ScaledIntegerNode( colorbox.get( "colorBlueMaximum" ) ).scaledValue();
            data3DHeader.colorLimits.colorBlueMinimum =
               ScaledIntegerNode( colorbox.get( "colorBlueMinimum" ) ).scaledValue();
         }
         else if ( colorbox.get( "colorRedMaximum" ).type() == TypeFloat )
         {
            data3DHeader.colorLimits.colorRedMaximum =
               FloatNode( colorbox.get( "colorRedMaximum" ) ).value();
            data3DHeader.colorLimits.colorRedMinimum =
               FloatNode( colorbox.get( "colorRedMinimum" ) ).value();
            data3DHeader.colorLimits.colorGreenMaximum =
               FloatNode( colorbox.get( "colorGreenMaximum" ) ).value();
            data3DHeader.colorLimits.colorGreenMinimum =
               FloatNode( colorbox.get( "colorGreenMinimum" ) ).value();
            data3DHeader.colorLimits.colorBlueMaximum =
               FloatNode( colorbox.get( "colorBlueMaximum" ) ).value();
            data3DHeader.colorLimits.colorBlueMinimum =
               FloatNode( colorbox.get( "colorBlueMinimum" ) ).value();
         }
         else if ( colorbox.get( "colorRedMaximum" ).type() == TypeInteger )
         {
            data3DHeader.colorLimits.colorRedMaximum =
               static_cast<double>( IntegerNode( colorbox.get( "colorRedMaximum" ) ).value() );
            data3DHeader.colorLimits.colorRedMinimum =
               static_cast<double>( IntegerNode( colorbox.get( "colorRedMinimum" ) ).value() );
            data3DHeader.colorLimits.colorGreenMaximum =
               static_cast<double>( IntegerNode( colorbox.get( "colorGreenMaximum" ) ).value() );
            data3DHeader.colorLimits.colorGreenMinimum =
               static_cast<double>( IntegerNode( colorbox.get( "colorGreenMinimum" ) ).value() );
            data3DHeader.colorLimits.colorBlueMaximum =
               static_cast<double>( IntegerNode( colorbox.get( "colorBlueMaximum" ) ).value() );
            data3DHeader.colorLimits.colorBlueMinimum =
               static_cast<double>( IntegerNode( colorbox.get( "colorBlueMinimum" ) ).value() );
         }
      }

      _readColourRanges( "colorRed", proto, data3DHeader.colorLimits.colorRedMinimum,
                         data3DHeader.colorLimits.colorRedMaximum );
      _readColourRanges( "colorGreen", proto, data3DHeader.colorLimits.colorGreenMinimum,
                         data3DHeader.colorLimits.colorGreenMaximum );
      _readColourRanges( "colorBlue", proto, data3DHeader.colorLimits.colorBlueMinimum,
                         data3DHeader.colorLimits.colorBlueMaximum );

      // E57_EXT_surface_normals
      // See: http://www.libe57.org/E57_EXT_surface_normals.txt
      if ( imf_.extensionsLookupPrefix( "nor" ) )
      {
         data3DHeader.pointFields.normalXField = proto.isDefined( "nor:normalX" );
         data3DHeader.pointFields.normalYField = proto.isDefined( "nor:normalY" );
         data3DHeader.pointFields.normalZField = proto.isDefined( "nor:normalZ" );
      }

      return true;
   }

   // This function returns the size of the point data
   bool ReaderImpl::GetData3DSizes( int64_t dataIndex, int64_t &row, int64_t &column,
                                    int64_t &pointsSize, int64_t &groupsSize, int64_t &countSize,
                                    bool &bColumnIndex ) const
   {
      int64_t elementSize = 0;

      row = 0;
      column = 0;
      pointsSize = 0;
      groupsSize = 0;
      countSize = 0;
      bColumnIndex = false;

      if ( !IsOpen() || ( dataIndex < 0 ) || ( dataIndex >= data3D_.childCount() ) )
      {
         return false;
      }

      const StructureNode scan( data3D_.get( dataIndex ) );
      const CompressedVectorNode points( scan.get( "points" ) );

      pointsSize = points.childCount();

      if ( scan.isDefined( "indexBounds" ) )
      {
         const StructureNode indexBounds( scan.get( "indexBounds" ) );

         if ( indexBounds.isDefined( "columnMaximum" ) )
         {
            column = IntegerNode( indexBounds.get( "columnMaximum" ) ).value() -
                     IntegerNode( indexBounds.get( "columnMinimum" ) ).value() + 1;
         }

         if ( indexBounds.isDefined( "rowMaximum" ) )
         {
            row = IntegerNode( indexBounds.get( "rowMaximum" ) ).value() -
                  IntegerNode( indexBounds.get( "rowMinimum" ) ).value() + 1;
         }
      }

      if ( scan.isDefined( "pointGroupingSchemes" ) )
      {
         const StructureNode pointGroupingSchemes( scan.get( "pointGroupingSchemes" ) );

         if ( pointGroupingSchemes.isDefined( "groupingByLine" ) )
         {
            const StructureNode groupingByLine( pointGroupingSchemes.get( "groupingByLine" ) );
            const StringNode idElementName( groupingByLine.get( "idElementName" ) );

            if ( idElementName.value() == "columnIndex" )
            {
               bColumnIndex = true;
            }

            const CompressedVectorNode groups( groupingByLine.get( "groups" ) );
            const StructureNode lineGroupRecord( groups.prototype() );

            groupsSize = groups.childCount();

            if ( lineGroupRecord.isDefined( "idElementValue" ) )
            {
               const IntegerNode integerIDElementValue( lineGroupRecord.get( "idElementValue" ) );

               elementSize = integerIDElementValue.maximum() - integerIDElementValue.minimum() + 1;
            }
            else if ( bColumnIndex )
            {
               elementSize = column;
            }
            else
            {
               elementSize = row;
            }

            if ( lineGroupRecord.isDefined( "pointCount" ) )
            {
               countSize = IntegerNode( lineGroupRecord.get( "pointCount" ) ).maximum();
            }
            else if ( bColumnIndex )
            {
               countSize = row;
            }
            else
            {
               countSize = column;
            }
         }
      }

      // if indexBounds is not given
      if ( row == 0 )
      {
         if ( bColumnIndex )
         {
            row = countSize;
         }
         else
         {
            row = elementSize;
         }
      }
      if ( column == 0 )
      {
         if ( bColumnIndex )
         {
            column = elementSize;
         }
         else
         {
            column = countSize;
         }
      }

      return true;
   }

   // Reads the group data
   bool ReaderImpl::ReadData3DGroupsData( int64_t dataIndex, size_t groupCount,
                                          int64_t *idElementValue, int64_t *startPointIndex,
                                          int64_t *pointCount ) const
   {
      if ( ( dataIndex < 0 ) || ( dataIndex >= data3D_.childCount() ) )
      {
         return false;
      }

      const StructureNode scan( data3D_.get( dataIndex ) );

      if ( !scan.isDefined( "pointGroupingSchemes" ) )
      {
         return false;
      }

      const StructureNode pointGroupingSchemes( scan.get( "pointGroupingSchemes" ) );

      if ( !pointGroupingSchemes.isDefined( "groupingByLine" ) )
      {
         return false;
      }

      const StructureNode groupingByLine( pointGroupingSchemes.get( "groupingByLine" ) );
      const StringNode idElementName( groupingByLine.get( "idElementName" ) );
      CompressedVectorNode groups( groupingByLine.get( "groups" ) );
      const StructureNode lineGroupRecord( groups.prototype() );
      const int64_t protoCount = lineGroupRecord.childCount();
      std::vector<SourceDestBuffer> groupSDBuffers;

      for ( int64_t protoIndex = 0; protoIndex < protoCount; protoIndex++ )
      {
         const ustring name = lineGroupRecord.get( protoIndex ).elementName();

         if ( ( name == "idElementValue" ) && lineGroupRecord.isDefined( "idElementValue" ) &&
              ( idElementValue != nullptr ) )
         {
            groupSDBuffers.emplace_back( imf_, "idElementValue", idElementValue, groupCount, true );
         }

         if ( ( name == "startPointIndex" ) && lineGroupRecord.isDefined( "startPointIndex" ) &&
              ( startPointIndex != nullptr ) )
         {
            groupSDBuffers.emplace_back( imf_, "startPointIndex", startPointIndex, groupCount,
                                         true );
         }

         if ( ( name == "pointCount" ) && lineGroupRecord.isDefined( "pointCount" ) &&
              ( pointCount != nullptr ) )
         {
            groupSDBuffers.emplace_back( imf_, "pointCount", pointCount, groupCount, true );
         }
      }

      CompressedVectorReader reader = groups.reader( groupSDBuffers );

      reader.read();
      reader.close();

      return true;
   }

   template <typename COORDTYPE>
   CompressedVectorReader ReaderImpl::SetUpData3DPointsData(
      int64_t dataIndex, size_t count, const Data3DPointsData_t<COORDTYPE> &buffers ) const
   {
      static_assert( std::is_floating_point<COORDTYPE>::value, "Floating point type required." );

      const StructureNode scan( data3D_.get( dataIndex ) );
      CompressedVectorNode points( scan.get( "points" ) );
      const StructureNode proto( points.prototype() );
      const int64_t protoCount = proto.childCount();
      std::vector<SourceDestBuffer> destBuffers;

      for ( int64_t protoIndex = 0; protoIndex < protoCount; protoIndex++ )
      {
         const ustring name = proto.get( protoIndex ).elementName();
         const NodeType type = proto.get( protoIndex ).type();
         const bool scaled = ( type == TypeScaledInteger );

         // E57_EXT_surface_normals
         ustring norExtUri;
         const bool haveNormalsExt = imf_.extensionsLookupPrefix( "nor", norExtUri );

         if ( ( name == "cartesianX" ) && proto.isDefined( "cartesianX" ) &&
              ( buffers.cartesianX != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "cartesianX", buffers.cartesianX, count, true, scaled );
         }
         else if ( ( name == "cartesianY" ) && proto.isDefined( "cartesianY" ) &&
                   ( buffers.cartesianY != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "cartesianY", buffers.cartesianY, count, true, scaled );
         }
         else if ( ( name == "cartesianZ" ) && proto.isDefined( "cartesianZ" ) &&
                   ( buffers.cartesianZ != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "cartesianZ", buffers.cartesianZ, count, true, scaled );
         }
         else if ( ( name == "cartesianInvalidState" ) &&
                   proto.isDefined( "cartesianInvalidState" ) &&
                   ( buffers.cartesianInvalidState != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "cartesianInvalidState", buffers.cartesianInvalidState,
                                      count, true );
         }
         else if ( ( name == "sphericalRange" ) && proto.isDefined( "sphericalRange" ) &&
                   ( buffers.sphericalRange != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "sphericalRange", buffers.sphericalRange, count, true,
                                      scaled );
         }
         else if ( ( name == "sphericalAzimuth" ) && proto.isDefined( "sphericalAzimuth" ) &&
                   ( buffers.sphericalAzimuth != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "sphericalAzimuth", buffers.sphericalAzimuth, count,
                                      true, scaled );
         }
         else if ( ( name == "sphericalElevation" ) && proto.isDefined( "sphericalElevation" ) &&
                   ( buffers.sphericalElevation != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "sphericalElevation", buffers.sphericalElevation, count,
                                      true, scaled );
         }
         else if ( ( name == "sphericalInvalidState" ) &&
                   proto.isDefined( "sphericalInvalidState" ) &&
                   ( buffers.sphericalInvalidState != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "sphericalInvalidState", buffers.sphericalInvalidState,
                                      count, true );
         }
         else if ( ( name == "rowIndex" ) && proto.isDefined( "rowIndex" ) &&
                   ( buffers.rowIndex != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "rowIndex", buffers.rowIndex, count, true );
         }
         else if ( ( name == "columnIndex" ) && proto.isDefined( "columnIndex" ) &&
                   ( buffers.columnIndex != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "columnIndex", buffers.columnIndex, count, true );
         }
         else if ( ( name == "returnIndex" ) && proto.isDefined( "returnIndex" ) &&
                   ( buffers.returnIndex != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "returnIndex", buffers.returnIndex, count, true );
         }
         else if ( ( name == "returnCount" ) && proto.isDefined( "returnCount" ) &&
                   ( buffers.returnCount != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "returnCount", buffers.returnCount, count, true );
         }
         else if ( ( name == "timeStamp" ) && proto.isDefined( "timeStamp" ) &&
                   ( buffers.timeStamp != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "timeStamp", buffers.timeStamp, count, true, scaled );
         }
         else if ( ( name == "isTimeStampInvalid" ) && proto.isDefined( "isTimeStampInvalid" ) &&
                   ( buffers.isTimeStampInvalid != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "isTimeStampInvalid", buffers.isTimeStampInvalid, count,
                                      true );
         }
         else if ( ( name == "intensity" ) && proto.isDefined( "intensity" ) &&
                   ( buffers.intensity != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "intensity", buffers.intensity, count, true, scaled );
         }
         else if ( ( name == "isIntensityInvalid" ) && proto.isDefined( "isIntensityInvalid" ) &&
                   ( buffers.isIntensityInvalid != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "isIntensityInvalid", buffers.isIntensityInvalid, count,
                                      true );
         }
         else if ( ( name == "colorRed" ) && proto.isDefined( "colorRed" ) &&
                   ( buffers.colorRed != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "colorRed", buffers.colorRed, count, true, scaled );
         }
         else if ( ( name == "colorGreen" ) && proto.isDefined( "colorGreen" ) &&
                   ( buffers.colorGreen != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "colorGreen", buffers.colorGreen, count, true, scaled );
         }
         else if ( ( name == "colorBlue" ) && proto.isDefined( "colorBlue" ) &&
                   ( buffers.colorBlue != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "colorBlue", buffers.colorBlue, count, true, scaled );
         }
         else if ( ( name == "isColorInvalid" ) && proto.isDefined( "isColorInvalid" ) &&
                   ( buffers.isColorInvalid != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "isColorInvalid", buffers.isColorInvalid, count, true );
         }
         else if ( haveNormalsExt && ( name == "nor:normalX" ) &&
                   proto.isDefined( "nor:normalX" ) && ( buffers.normalX != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "nor:normalX", buffers.normalX, count, true, scaled );
         }
         else if ( haveNormalsExt && ( name == "nor:normalY" ) &&
                   proto.isDefined( "nor:normalY" ) && ( buffers.normalY != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "nor:normalY", buffers.normalY, count, true, scaled );
         }
         else if ( haveNormalsExt && ( name == "nor:normalZ" ) &&
                   proto.isDefined( "nor:normalZ" ) && ( buffers.normalZ != nullptr ) )
         {
            destBuffers.emplace_back( imf_, "nor:normalZ", buffers.normalZ, count, true, scaled );
         }
      }

      CompressedVectorReader reader = points.reader( destBuffers );

      return reader;
   }

   int64_t ReaderImpl::GetData3DCount() const
   {
      return data3D_.childCount();
   }

   StructureNode ReaderImpl::GetRawE57Root() const
   {
      return root_;
   }

   VectorNode ReaderImpl::GetRawData3D() const
   {
      return data3D_;
   }

   VectorNode ReaderImpl::GetRawImages2D() const
   {
      return images2D_;
   }

   ImageFile ReaderImpl::GetRawIMF() const
   {
      return imf_;
   }

   // Explicit template instantiation
   template CompressedVectorReader ReaderImpl::SetUpData3DPointsData(
      int64_t dataIndex, size_t pointCount, const Data3DPointsData_t<float> &buffers ) const;

   template CompressedVectorReader ReaderImpl::SetUpData3DPointsData(
      int64_t dataIndex, size_t pointCount, const Data3DPointsData_t<double> &buffers ) const;

} // end namespace e57
