// Copyright (C) Stichting Deltares 2016. All rights reserved.
//
// This file is part of Ringtoets.
//
// Ringtoets is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
// All names, logos, and references to "Deltares" are registered trademarks of
// Stichting Deltares and remain full property of Stichting Deltares at all times.
// All rights reserved.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Core.Common.Base.Geometry;
using Core.Common.IO.Exceptions;
using Core.Common.IO.Readers;
using Core.Common.Utils;
using Core.Common.Utils.Builders;
using Ringtoets.Piping.IO.Properties;
using Ringtoets.Piping.Primitives;
using UtilsResources = Core.Common.Utils.Properties.Resources;
namespace Ringtoets.Piping.IO.SurfaceLines
{
///
/// File reader for a plain text file in comma-separated values format (*.csv), containing
/// data specifying surfacelines.
/// Expects data to be specified in the following format:
/// {ID};X1;Y1;Z1...;(Xn;Yn;Zn)
/// Where {ID} has to be a particular accepted text, and n triplets of doubles form the
/// 3D coordinates defining the geometric shape of the surfaceline.
///
public class PipingSurfaceLinesCsvReader : IDisposable
{
private const char separator = ';';
private readonly string[] acceptableLowerCaseIdNames =
{
"profielnaam",
"locationid"
};
private readonly string filePath;
private readonly string[] expectedFirstCoordinateHeader =
{
"x1",
"y1",
"z1"
};
private StreamReader fileReader;
private int lineNumber;
private int idNameColumnIndex;
private int startGeometryColumnIndex;
///
/// Initializes a new instance of the class
/// and opens a given file path.
///
/// The path to the file to be read.
/// is invalid.
public PipingSurfaceLinesCsvReader(string path)
{
FileUtils.ValidateFilePath(path);
filePath = path;
}
///
/// Reads the file to determine the number of available
/// data rows.
///
/// A value greater than or equal to 0.
/// A critical error has occurred, which may be caused by:
///
/// - The file cannot be found at specified path.
/// - The specified path is invalid, such as being on an unmapped drive.
/// - Some other I/O related issue occurred, such as: path includes an incorrect
/// or invalid syntax for file name, directory name, or volume label.
/// - There is insufficient memory to allocate a buffer for the returned string.
/// - The file incompatible for importing surface lines.
///
///
public int GetSurfaceLinesCount()
{
using (var reader = StreamReaderHelper.InitializeStreamReader(filePath))
{
ValidateHeader(reader, 1);
return CountNonEmptyLines(reader, 2);
}
}
///
/// Reads and consumes the next data row which contains a surface line, parsing the data to create an instance
/// of .
///
/// Return the parsed surfaceline, or null when at the end of the file.
/// A critical error has occurred, which may be caused by:
///
/// - The file cannot be found at specified path.
/// - The specified path is invalid, such as being on an unmapped drive.
/// - Some other I/O related issue occurred, such as: path includes an incorrect
/// or invalid syntax for file name, directory name, or volume label.
/// - There is insufficient memory to allocate a buffer for the returned string.
/// - The file incompatible for importing surface lines.
///
///
/// A parse error has occurred for the current row, which may be caused by:
///
/// - The row doesn't use ';' as separator character.
/// - The row contains a coordinate value that cannot be parsed as a double.
/// - The row contains a number that is too big or too small to be represented with a double.
/// - The row is missing an identifier value.
/// - The row is missing values to form a surface line point.
///
///
public RingtoetsPipingSurfaceLine ReadSurfaceLine()
{
if (fileReader == null)
{
fileReader = StreamReaderHelper.InitializeStreamReader(filePath);
ValidateHeader(fileReader, 1);
lineNumber = 2;
}
var readText = ReadNextNonEmptyLine();
if (readText != null)
{
try
{
return CreateRingtoetsPipingSurfaceLine(readText);
}
finally
{
lineNumber++;
}
}
return null;
}
public void Dispose()
{
if (fileReader != null)
{
fileReader.Dispose();
fileReader = null;
}
}
///
/// Reads lines from file until the first non-white line is hit.
///
/// The next line which is not a white line, or null when no non-white line could be found before the
/// end of file.
private string ReadNextNonEmptyLine()
{
string readText;
while ((readText = ReadLineAndHandleIOExceptions(fileReader, lineNumber)) != null)
{
if (string.IsNullOrWhiteSpace(readText))
{
lineNumber++;
}
else
{
break;
}
}
return readText;
}
private RingtoetsPipingSurfaceLine CreateRingtoetsPipingSurfaceLine(string readText)
{
var tokenizedString = TokenizeString(readText);
var surfaceLineName = GetSurfaceLineName(tokenizedString);
var points = GetSurfaceLinePoints(tokenizedString, surfaceLineName);
var surfaceLine = new RingtoetsPipingSurfaceLine
{
Name = surfaceLineName
};
surfaceLine.SetGeometry(points);
CheckIfGeometryIsValid(surfaceLine);
return surfaceLine;
}
///
/// Checks if the geometry defining the surface line is valid.
///
/// The surface line to be checked.
/// Surface line geometry is invalid.
private void CheckIfGeometryIsValid(RingtoetsPipingSurfaceLine surfaceLine)
{
CheckZeroLength(surfaceLine);
CheckReclinging(surfaceLine);
}
private void CheckZeroLength(RingtoetsPipingSurfaceLine surfaceLine)
{
Point3D lastPoint = null;
foreach (var point in surfaceLine.Points)
{
if (lastPoint != null)
{
if (!Equals(lastPoint, point))
{
return;
}
}
lastPoint = point;
}
throw CreateLineParseException(lineNumber, surfaceLine.Name, Resources.PipingSurfaceLinesCsvReader_ReadLine_SurfaceLine_has_zero_length);
}
private void CheckReclinging(RingtoetsPipingSurfaceLine surfaceLine)
{
double[] lCoordinates = surfaceLine.ProjectGeometryToLZ().Select(p => p.X).ToArray();
for (int i = 1; i < lCoordinates.Length; i++)
{
if (lCoordinates[i - 1] > lCoordinates[i])
{
throw CreateLineParseException(lineNumber, surfaceLine.Name, Resources.PipingSurfaceLinesCsvReader_ReadLine_SurfaceLine_has_reclining_geometry);
}
}
}
///
/// Tokenizes a string using a separator character up to the first empty field.
///
/// The text.
/// The tokenized parts.
/// lacks separator character.
private string[] TokenizeString(string readText)
{
if (!readText.Contains(separator))
{
throw CreateLineParseException(lineNumber, string.Format(Resources.PipingSurfaceLinesCsvReader_ReadLine_Line_lacks_separator_0_,
separator));
}
return readText.Split(separator)
.TakeWhile(text => !string.IsNullOrEmpty(text))
.ToArray();
}
///
/// Gets the 3D surface line points.
///
/// The tokenized string.
/// The name of the surface line for which geometry points are retrieved.
/// Set of all 3D world coordinate points.
/// A parse error has occurred for the current row, which may be caused by:
///
/// - contains a coordinate value that cannot be parsed as a double.
/// - contains a number that is too big or too small to be represented with a double.
/// - is missing coordinate values to define a proper surface line point.
///
///
private IEnumerable GetSurfaceLinePoints(string[] tokenizedString, string surfaceLineName)
{
const int expectedValuesForPoint = 3;
var worldCoordinateValues = ParseWorldCoordinateValuesAndHandleParseErrors(tokenizedString, surfaceLineName);
if (worldCoordinateValues.Length%expectedValuesForPoint != 0)
{
throw CreateLineParseException(lineNumber, surfaceLineName, Resources.PipingSurfaceLinesCsvReader_ReadLine_SurfaceLine_lacks_values_for_coordinate_triplet);
}
int coordinateCount = worldCoordinateValues.Length/expectedValuesForPoint;
var points = new Point3D[coordinateCount];
for (int i = 0; i < coordinateCount; i++)
{
var x = worldCoordinateValues[i*expectedValuesForPoint];
var y = worldCoordinateValues[i*expectedValuesForPoint + 1];
var z = worldCoordinateValues[i*expectedValuesForPoint + 2];
points[i] = new Point3D(x, y, z);
}
return points;
}
///
/// Gets the name of the surface line.
///
/// The tokenized string from which the name should be extracted.
/// The name of the surface line.
/// Id value is null or empty.
private string GetSurfaceLineName(IList tokenizedString)
{
var name = tokenizedString.Any() ? tokenizedString[idNameColumnIndex].Trim() : string.Empty;
if (string.IsNullOrEmpty(name))
{
throw CreateLineParseException(lineNumber, Resources.PipingSurfaceLinesCsvReader_ReadLine_Line_lacks_ID);
}
return name;
}
///
/// Parses the world coordinate values and handles parse errors.
///
/// The tokenized string.
/// The name of the surface line whose coordinate values are being parsed.
///
/// A parse error has occurred for the current row, which may be caused by:
///
/// - The row contains a coordinate value that cannot be parsed as a double.
/// - The row contains a number that is too big or too small to be represented with a double.
///
///
private double[] ParseWorldCoordinateValuesAndHandleParseErrors(string[] tokenizedString, string surfaceLineName)
{
try
{
return tokenizedString.Skip(startGeometryColumnIndex)
.Select(ts => double.Parse(ts, CultureInfo.InvariantCulture))
.ToArray();
}
catch (FormatException e)
{
throw CreateLineParseException(lineNumber, surfaceLineName, Resources.Error_SurfaceLine_has_not_double, e);
}
catch (OverflowException e)
{
throw CreateLineParseException(lineNumber, surfaceLineName, Resources.Error_SurfaceLine_parsing_causes_overflow, e);
}
}
///
/// Validates the header of the file.
///
/// The reader, which is currently at the header row.
/// Row index used in error messaging.
/// The header is not in the required format.
private void ValidateHeader(TextReader reader, int currentLine)
{
var header = ReadLineAndHandleIOExceptions(reader, currentLine);
if (header != null)
{
if (!IsHeaderValid(header))
{
throw CreateCriticalFileReadException(currentLine, Resources.PipingSurfaceLinesCsvReader_File_invalid_header);
}
}
else
{
throw CreateCriticalFileReadException(currentLine, UtilsResources.Error_File_empty);
}
}
///
/// Throws a configured instance of .
///
/// The line number being read.
/// The critical error message.
/// Optional: exception that caused this exception to be thrown.
/// New with message and inner exception set.
private CriticalFileReadException CreateCriticalFileReadException(int currentLine, string criticalErrorMessage, Exception innerException = null)
{
string locationDescription = string.Format(UtilsResources.TextFile_On_LineNumber_0_, currentLine);
var message = new FileReaderErrorMessageBuilder(filePath).WithLocation(locationDescription)
.Build(criticalErrorMessage);
return new CriticalFileReadException(message, innerException);
}
///
/// Throws a configured instance of .
///
/// The line number being read.
/// The critical error message.
/// New with message set.
private LineParseException CreateLineParseException(int currentLine, string lineParseErrorMessage)
{
string locationDescription = string.Format(UtilsResources.TextFile_On_LineNumber_0_, currentLine);
var message = new FileReaderErrorMessageBuilder(filePath).WithLocation(locationDescription)
.Build(lineParseErrorMessage);
return new LineParseException(message);
}
///
/// Throws a configured instance of .
///
/// The line number being read.
/// The name of the surfaceline being read.
/// The critical error message.
/// Optional: exception that caused this exception to be thrown.
/// New with message and inner exceptions set.
private LineParseException CreateLineParseException(int currentLine, string surfaceLineName, string lineParseErrorMessage, Exception innerException = null)
{
string locationDescription = string.Format(UtilsResources.TextFile_On_LineNumber_0_, currentLine);
string subjectDescription = string.Format(Resources.PipingSurfaceLinesCsvReader_SurfaceLineName_0_, surfaceLineName);
var message = new FileReaderErrorMessageBuilder(filePath).WithLocation(locationDescription)
.WithSubject(subjectDescription)
.Build(lineParseErrorMessage);
return new LineParseException(message, innerException);
}
///
/// Counts the remaining non-empty lines.
///
/// The reader at the row from which counting should start.
/// The current line, used for error messaging.
/// An integer greater than or equal to 0, being the number of surfaceline rows.
/// An I/O exception occurred.
private int CountNonEmptyLines(TextReader reader, int currentLine)
{
int count = 0;
int lineNumberForMessage = currentLine;
string line;
while ((line = ReadLineAndHandleIOExceptions(reader, lineNumberForMessage)) != null)
{
if (!string.IsNullOrWhiteSpace(line))
{
count++;
}
lineNumberForMessage++;
}
return count;
}
///
/// Reads the next line and handles I/O exceptions.
///
/// The opened text file reader.
/// Row number for error messaging.
/// The read line, or null when at the end of the file.
/// An critical I/O exception occurred.
private string ReadLineAndHandleIOExceptions(TextReader reader, int currentLine)
{
try
{
return reader.ReadLine();
}
catch (OutOfMemoryException e)
{
throw CreateCriticalFileReadException(currentLine, UtilsResources.Error_Line_too_big_for_RAM, e);
}
catch (IOException e)
{
var message = new FileReaderErrorMessageBuilder(filePath).Build(string.Format(UtilsResources.Error_General_IO_ErrorMessage_0_, e.Message));
throw new CriticalFileReadException(message, e);
}
}
private bool IsHeaderValid(string header)
{
var tokenizedHeader = header.Split(separator).Select(s => s.Trim().ToLowerInvariant()).ToArray();
// Check for valid id:
DetermineIdNameColumnIndex(tokenizedHeader);
if (idNameColumnIndex == -1)
{
return false;
}
// Check for valid 1st coordinate in header:
DetermineStartGeometryColumnIndex(tokenizedHeader);
if (startGeometryColumnIndex == -1)
{
return false;
}
bool valid = true;
for (int i = 0; i < expectedFirstCoordinateHeader.Length && valid; i++)
{
valid = tokenizedHeader[startGeometryColumnIndex + i].Equals(expectedFirstCoordinateHeader[i]);
}
return valid;
}
private void DetermineStartGeometryColumnIndex(string[] tokenizedHeader)
{
startGeometryColumnIndex = Array.IndexOf(tokenizedHeader, expectedFirstCoordinateHeader[0]);
}
private void DetermineIdNameColumnIndex(string[] tokenizedHeader)
{
idNameColumnIndex = -1;
foreach (string name in acceptableLowerCaseIdNames)
{
idNameColumnIndex = Array.IndexOf(tokenizedHeader, name);
if (idNameColumnIndex > -1)
{
break;
}
}
}
}
}