// 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; } } } } }