// Copyright (C) Stichting Deltares 2017. 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.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Core.Common.Base; using Core.Common.Base.Data; using Core.Common.Base.Geometry; using Core.Common.Base.IO; using Core.Common.Utils; using Core.Common.Utils.Builders; using Ringtoets.Common.Data.DikeProfiles; using Ringtoets.Common.IO.Exceptions; using Ringtoets.Common.IO.Properties; using CoreCommonUtilsResources = Core.Common.Utils.Properties.Resources; using UtilsResources = Core.Common.Utils.Properties.Resources; namespace Ringtoets.Common.IO.DikeProfiles { /// /// Reader responsible for reading the data for a dike profile from a .prfl file. /// public class DikeProfileDataReader { [Flags] private enum Keywords { None = 0, VERSIE = 1, ID = 2, RICHTING = 4, DAM = 8, DAMHOOGTE = 16, VOORLAND = 32, DAMWAND = 64, KRUINHOOGTE = 128, DIJK = 256, MEMO = 512 } private static readonly Range orientationValidityRange = new Range(0, 360); private static readonly Range roughnessValidityRange = new Range(0.5, 1.0); private readonly string[] acceptedIds; private string fileBeingRead; private Keywords readKeywords; /// /// Initializes a new instance of . /// /// The accepted ids the file id must have to pass validation. /// Thrown when is null. public DikeProfileDataReader(string[] acceptedIds) { if (acceptedIds == null) { throw new ArgumentNullException(nameof(acceptedIds)); } this.acceptedIds = acceptedIds; } /// /// Reads a *.prfl file containing dike profile data. /// /// The file path. /// The read . /// Thrown when is invalid. /// Thrown when: /// /// refers to a file that doesn't exist. /// A piece of text from the file cannot be converted into the expected variable type. /// A critical I/O exception occurred while attempting to read a line in the file. /// A converted value is invalid. /// The file is incomplete. /// A keyword is defined more then once. /// The geometry points for either the dike or foreshore do not have monotonically /// increasing X-coordinates. /// An unexpected piece of text has been encountered in the file. /// /// Thrown when the read id is not an accepted id. public DikeProfileData ReadDikeProfileData(string filePath) { IOUtils.ValidateFilePath(filePath); if (!File.Exists(filePath)) { string message = new FileReaderErrorMessageBuilder(filePath) .Build(CoreCommonUtilsResources.Error_File_does_not_exist); throw new CriticalFileReadException(message); } var data = new DikeProfileData(); readKeywords = Keywords.None; using (var reader = new StreamReader(filePath)) { fileBeingRead = filePath; string text; var lineNumber = 0; while ((text = ReadLineAndHandleIOExceptions(reader, ++lineNumber)) != null) { if (string.IsNullOrWhiteSpace(text)) { continue; } if (TryReadVersion(text, lineNumber)) { continue; } if (TryReadId(text, data, lineNumber)) { continue; } if (TryReadOrientation(text, data, lineNumber)) { continue; } if (TryReadDamType(text, data, lineNumber)) { continue; } if (TryReadSheetPileType(text, data, lineNumber)) { continue; } if (TryReadDamHeight(text, data, lineNumber)) { continue; } if (TryReadDikeHeight(text, data, lineNumber)) { continue; } if (TryReadDikeRoughnessPoints(text, data, reader, ref lineNumber)) { continue; } if (TryReadForeshoreRoughnessPoints(text, data, reader, ref lineNumber)) { continue; } if (TryReadMemo(text, data, reader)) { continue; } HandleUnexpectedText(text, lineNumber); } } ValidateNoMissingKeywords(); return data; } /// /// Attempts to match the given text to a VERSIE key-value pair. If a match is found, /// the value is parsed and validated. /// /// The text. /// The line number. /// True if the text matches a VERSIE key-value pair and has been /// validated successfully; false otherwise. /// The value after the VERSIE key does /// not represent a valid version code or has already been defined or the version /// is not supported by this reader. private bool TryReadVersion(string text, int lineNumber) { Match versionMatch = new Regex(@"^VERSIE(\s+(?.+?)?)?\s*$").Match(text); if (versionMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.VERSIE, lineNumber); string readVersionText = versionMatch.Groups["version"].Value; Version fileVersion = ParseVersion(lineNumber, readVersionText); ValidateVersion(fileVersion, lineNumber); readKeywords |= Keywords.VERSIE; return true; } return false; } /// /// Parses the version code from a piece of text. /// /// The line number. /// The text to parse. /// The read version code. /// Thrown when /// does not represent a valid version code. private Version ParseVersion(int lineNumber, string readVersionText) { try { return new Version(readVersionText); } catch (FormatException e) { string message = Resources.DikeProfileDataReader_ValidateVersion_Only_version_four_zero_supported; throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = Resources.DikeProfileDataReader_ValidateVersion_Only_version_four_zero_supported; throw CreateCriticalFileReadException(lineNumber, message, e); } catch (ArgumentException e) { string message = Resources.DikeProfileDataReader_ValidateVersion_Only_version_four_zero_supported; throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Validates the version. /// /// The file version. /// The line number. /// Thrown when /// is not supported by this reader. private void ValidateVersion(Version fileVersion, int lineNumber) { if (!fileVersion.Equals(new Version(4, 0))) { string message = Resources.DikeProfileDataReader_ValidateVersion_Only_version_four_zero_supported; throw CreateCriticalFileReadException(lineNumber, message); } } /// /// Attempts to match the given text to an ID key-value pair. If a match is found, /// the value is parsed and validated. /// /// The text. /// The data to be updated. /// The line number. /// True if the text matches an ID key-value pair and has been /// validated successfully; false otherwise. /// The value after the ID key does /// not represent an id or has already been defined. private bool TryReadId(string text, DikeProfileData data, int lineNumber) { Match idMatch = new Regex(@"^ID(\s+(?.+?)?)?\s*$").Match(text); if (idMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.ID, lineNumber); string readIdText = idMatch.Groups["id"].Value; ValidateId(readIdText, lineNumber); data.Id = readIdText; readKeywords |= Keywords.ID; return true; } return false; } /// /// Validates the identifier. /// /// The id. /// The line number. /// Thrown when is not /// present or has a whitespace. /// Thrown when is not an accepted id. private void ValidateId(string id, int lineNumber) { if (string.IsNullOrWhiteSpace(id)) { string message = string.Format(Resources.DikeProfileDataReader_ValidateId_Id_0_not_valid, id); throw CreateCriticalFileReadException(lineNumber, message); } if (id.Any(char.IsWhiteSpace)) { string message = string.Format(Resources.DikeProfileDataReader_ValidateId_Id_0_has_unsupported_white_space, id); throw CreateCriticalFileReadException(lineNumber, message); } if (!acceptedIds.Contains(id)) { string message = string.Format(Resources.DikeProfileDataReader_ValidateId_Id_0_not_valid, id); throw new CriticalFileValidationException(message); } } /// /// Attempts to match the given text to a RICHTING key-value pair. If a match is /// found, the value is parsed and validated. If valid, the value is stored. /// /// The text. /// The data to be updated. /// The line number. /// True if the text matches a RICHTING key-value pair and has been /// validated successfully; false otherwise. /// The value after the RICHTING key does /// not represent a valid number or has already been defined or the orientation value /// is not in the range [0, 360]. private bool TryReadOrientation(string text, DikeProfileData data, int lineNumber) { Match orientationMatch = new Regex(@"^RICHTING(\s+(?.+?)?)?\s*$").Match(text); if (orientationMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.RICHTING, lineNumber); string readOrientationText = orientationMatch.Groups["orientation"].Value; double orientation = ParseOrientation(lineNumber, readOrientationText); ValidateOrientation(orientation, lineNumber); data.Orientation = orientation; readKeywords |= Keywords.RICHTING; return true; } return false; } /// /// Parses the orientation from a piece of text. /// /// The line number. /// The text. /// The value corresponding to the RICHTING key. /// Thrown when /// does not represent a valid number. private double ParseOrientation(int lineNumber, string readOrientationText) { try { return double.Parse(readOrientationText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseOrientation_Orientation_0_not_double, readOrientationText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseOrientation_Orientation_0_overflows, readOrientationText); throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Validates the orientation. /// /// The orientation. /// The line number. /// Thrown when /// is outside the [0, 360] range. private void ValidateOrientation(double orientation, int lineNumber) { if (!orientationValidityRange.InRange(orientation)) { string rangeText = orientationValidityRange.ToString(FormattableConstants.ShowAtLeastOneDecimal, CultureInfo.CurrentCulture); string message = string.Format(Resources.DikeProfileDataReader_ValidateOrientation_Orientation_0_must_be_in_Range_1_, orientation, rangeText); throw CreateCriticalFileReadException(lineNumber, message); } } /// /// Attempts to match the given text to a DAM key-value pair. If a match is found, /// the value is parsed and validated. If valid, the value is stored. /// /// The text. /// The data to be updated. /// The line number. /// True if the text matches a DAM key-value pair and has been /// validated successfully; false otherwise. /// The value after the DAM key does /// not represent a valid value or has already been defined. private bool TryReadDamType(string text, DikeProfileData data, int lineNumber) { Match damTypeMatch = new Regex(@"^DAM(\s+(?.+?)?)?\s*$").Match(text); if (damTypeMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.DAM, lineNumber); string readDamTypeText = damTypeMatch.Groups["damtype"].Value; DamType damType = ParseDamType(lineNumber, readDamTypeText); data.DamType = damType; readKeywords |= Keywords.DAM; return true; } return false; } /// /// Parses the dam-type from a piece of text. /// /// The line number. /// The text. /// The read dam-type. /// Thrown when /// does not represent a . private DamType ParseDamType(int lineNumber, string readDamTypeText) { int damTypeValue; try { damTypeValue = int.Parse(readDamTypeText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseDamType_DamType_0_must_be_in_range, readDamTypeText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseDamType_DamType_0_must_be_in_range, readDamTypeText); throw CreateCriticalFileReadException(lineNumber, message, e); } if (!CanSafelyCastToEnum(damTypeValue)) { string message = string.Format(Resources.DikeProfileDataReader_ParseDamType_DamType_0_must_be_in_range, damTypeValue); throw CreateCriticalFileReadException(lineNumber, message); } return (DamType) damTypeValue; } /// /// Attempts to match the given text to a DAMWAND key-value pair. If a match is /// found, the value is parsed and validated. If valid, the value is stored. /// /// The text. /// The data to be updated. /// The line number. /// True if the text matches a DAMWAND key-value pair and has been /// validated successfully; false otherwise. /// The value after the DAMWAND key /// does not represent a valid value or has already been defined. private bool TryReadSheetPileType(string text, DikeProfileData data, int lineNumber) { Match profileTypeMatch = new Regex(@"^DAMWAND(\s+(?.+?)?)?\s*$").Match(text); if (profileTypeMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.DAMWAND, lineNumber); string readSheetPileTypeText = profileTypeMatch.Groups["sheetpiletype"].Value; SheetPileType sheetPileType = ParseSheetPileType(lineNumber, readSheetPileTypeText); data.SheetPileType = sheetPileType; readKeywords |= Keywords.DAMWAND; return true; } return false; } /// /// Parses the sheet piling type from a piece of text. /// /// The line number. /// The text. /// The read sheet piling type. /// Thrown when /// does not represent a . private SheetPileType ParseSheetPileType(int lineNumber, string readSheetPileTypeText) { int sheetPileTypeValue; try { sheetPileTypeValue = int.Parse(readSheetPileTypeText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseSheetPileType_SheetPileType_0_must_be_in_range, readSheetPileTypeText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseSheetPileType_SheetPileType_0_must_be_in_range, readSheetPileTypeText); throw CreateCriticalFileReadException(lineNumber, message, e); } if (!CanSafelyCastToEnum(sheetPileTypeValue)) { string message = string.Format(Resources.DikeProfileDataReader_ParseSheetPileType_SheetPileType_0_must_be_in_range, sheetPileTypeValue); throw CreateCriticalFileReadException(lineNumber, message); } return (SheetPileType) sheetPileTypeValue; } /// /// Attempts to match the given text to a DAMHOOGTE key-value pair. If a match is /// found, the value is parsed. /// /// The text. /// The data to be updated. /// The line number. /// True if the text matches a DAMHOOGTE key-value pair and has been /// validated successfully; false otherwise. /// The value after the DAMHOOGTE key /// does not represent a valid number or has already been defined. private bool TryReadDamHeight(string text, DikeProfileData data, int lineNumber) { Match damHeightMatch = new Regex(@"^DAMHOOGTE(\s+(?.+?)?)?\s*$").Match(text); if (damHeightMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.DAMHOOGTE, lineNumber); string readDamHeightText = damHeightMatch.Groups["damheight"].Value; double damHeight = ParseDamHeight(readDamHeightText, lineNumber); data.DamHeight = damHeight; readKeywords |= Keywords.DAMHOOGTE; return true; } return false; } /// /// Parses the height of the dam from a piece of text. /// /// The line number. /// The text. /// The height of the dam. /// Thrown when /// does not represent a number. private double ParseDamHeight(string readDamHeightText, int lineNumber) { try { return double.Parse(readDamHeightText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseDamHeight_DamHeight_0_not_number, readDamHeightText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseDamHeight_DamHeight_0_overflows, readDamHeightText); throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Attempts to match the given text to a KRUINHOOGTE key-value pair. If a match is /// found, the value is parsed. /// /// The text. /// The data to be updated. /// The line number. /// True if the text matches a KRUINHOOGTE key-value pair and has been /// validated successfully; false otherwise. /// The value after the KRUINHOOGTE key /// does not represent a valid number or has already been defined. private bool TryReadDikeHeight(string text, DikeProfileData data, int lineNumber) { Match crestLevelMatch = new Regex(@"^KRUINHOOGTE(\s+(?.+?)?)?\s*$").Match(text); if (crestLevelMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.KRUINHOOGTE, lineNumber); string readDikeHeightText = crestLevelMatch.Groups["dikeheight"].Value; double crestLevel = ParseDikeHeight(lineNumber, readDikeHeightText); data.DikeHeight = crestLevel; readKeywords |= Keywords.KRUINHOOGTE; return true; } return false; } /// /// Parses the height of the dike from a piece of text. /// /// The line number. /// The text. /// The height of the dike. /// Thrown when /// does not represent a number. private double ParseDikeHeight(int lineNumber, string readDikeHeightText) { try { return double.Parse(readDikeHeightText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseDikeHeight_DikeHeight_0_not_number, readDikeHeightText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseDikeHeight_DikeHeight_0_overflows, readDikeHeightText); throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Attempts to match the given text to a DIJK key-value pair. If a match is /// found, the data block is being read. If valid, the value is stored. /// /// The text of the DIJK key-value pair. /// The data to be updated. /// The reader of the file. /// The line number. /// True if the text matches a DIJK key-value pair and has been /// validated successfully; false otherwise. /// Thrown when /// /// The value after the DIJK key does not represent a valid number. /// Any of read the parameters in the following data block is invalid. /// The keyword has already been defined. /// The X-coordinates of the dike are not monotonically increasing. /// private bool TryReadDikeRoughnessPoints(string text, DikeProfileData data, TextReader reader, ref int lineNumber) { Match dikeGeometryMatch = new Regex(@"^DIJK(\s+(?.+?)?)?\s*$").Match(text); if (dikeGeometryMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.DIJK, lineNumber); string readDikeGeometryCountText = dikeGeometryMatch.Groups["dikegeometry"].Value; int numberOfPoints = ParseNumberOfDikePoints(lineNumber, readDikeGeometryCountText); ValidateDikePointCount(numberOfPoints, lineNumber); if (numberOfPoints == 0) { readKeywords |= Keywords.DIJK; return true; } data.DikeGeometry = new RoughnessPoint[numberOfPoints]; for (var i = 0; i < numberOfPoints; i++) { lineNumber++; text = ReadLineAndHandleIOExceptions(reader, lineNumber); if (string.IsNullOrWhiteSpace(text)) { string message = string.Format(Resources.DikeProfileDataReader_TryReadDikeRoughnessPoints_DikeCount_0_does_not_correspond_ExpectedCount_1_, i, numberOfPoints); throw CreateCriticalFileReadException(lineNumber, message); } RoughnessPoint roughnessPoint = ReadRoughnessPoint(text, lineNumber); data.DikeGeometry[i] = roughnessPoint; if (i > 0) { ValidateDikePointsAreMonotonicallyIncreasing(roughnessPoint.Point, data.DikeGeometry[i - 1].Point, lineNumber); } } readKeywords |= Keywords.DIJK; return true; } return false; } /// /// Parses the number of dike roughness points from a piece of text representing /// a DIJK key-value pair. /// /// The line number. /// The DIJK key-value pair text. /// The number of dike roughness points. /// Thrown when /// does not represent a number. private int ParseNumberOfDikePoints(int lineNumber, string readDikeGeometryCountText) { try { return int.Parse(readDikeGeometryCountText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseNumberOfDikeElements_DijkCount_0_not_integer, readDikeGeometryCountText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseNumberOfDikeElements_DikeCount_0_overflows, readDikeGeometryCountText); throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Validates the number of dike points to be read from file. /// /// The number of elements. /// The line number. /// Thrown when /// is negative. private void ValidateDikePointCount(int numberOfElements, int lineNumber) { if (numberOfElements < 0) { string message = string.Format(Resources.DikeProfileDataReader_ReadDikeProfileData_DikeCount_cannot_be_negative, numberOfElements); throw CreateCriticalFileReadException(lineNumber, message); } } /// /// Matches the given text to a value triplet representing a roughness point. If /// a match is found, the values are parsed and validated. If valid, the resulting /// based on the values will be returned. /// /// The text. /// The line number. /// A instance built using data in . /// Any parameter value in the roughness /// point triplet does not represent a valid number or the roughness parameter is /// not valid. private RoughnessPoint ReadRoughnessPoint(string text, int lineNumber) { Match roughnessSectionDataMatch = new Regex(@"^(\s*)?(?\S+)\s+(?\S+)\s+(?\S+)\s*$").Match(text); if (!roughnessSectionDataMatch.Success) { string message = string.Format(Resources.DikeProfileDataReader_ReadRoughnessPoint_Line_0_not_x_y_roughness_definition, text); throw CreateCriticalFileReadException(lineNumber, message); } string readLocalXText = roughnessSectionDataMatch.Groups["localx"].Value; double localX = ParseRoughnessPointParameter(readLocalXText, Resources.DikeProfileDataReader_ReadRoughnessPoint_X_DisplayName, lineNumber); string readLocalZText = roughnessSectionDataMatch.Groups["localz"].Value; double localZ = ParseRoughnessPointParameter(readLocalZText, Resources.DikeProfileDataReader_ReadRoughnessPoint_Z_DisplayName, lineNumber); string readRoughnessText = roughnessSectionDataMatch.Groups["roughness"].Value; double roughness = ParseRoughnessPointParameter(readRoughnessText, Resources.DikeProfileDataReader_ReadRoughnessPoint_Roughness_DisplayName, lineNumber); ValidateRoughness(roughness, lineNumber); return new RoughnessPoint(new Point2D(localX, localZ), roughness); } /// /// Parses some double parameter from a piece of text. /// /// The name of the parameter as shown to the user. /// The line number. /// The value text to be parsed. /// The parameter value. /// Thrown when /// does not represent a number. private double ParseRoughnessPointParameter(string readParameterText, string parameterName, int lineNumber) { try { return double.Parse(readParameterText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseRoughnessPointParameter_ParameterName_0_Value_1_not_number, parameterName, readParameterText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseRoughnessPointParameter_ParameterName_0_Value_1_overflows, parameterName, readParameterText); throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Validates the roughness. /// /// The roughness. /// The line number. /// Thrown when /// is outside the range [0.5, 1]. private void ValidateRoughness(double roughness, int lineNumber) { if (!roughnessValidityRange.InRange(roughness)) { string rangeText = roughnessValidityRange.ToString(FormattableConstants.ShowAtLeastOneDecimal, CultureInfo.CurrentCulture); string message = string.Format(Resources.DikeProfileDataReader_ReadRoughnessPoint_Roughness_0_must_be_Range_1_, roughness, rangeText); throw CreateCriticalFileReadException(lineNumber, message); } } /// /// Validates that two dike points are monotonically increasing. /// /// The current point. /// The previous point. /// The line number. /// Thrown when /// has an X-coordinate before or equal to that of . private void ValidateDikePointsAreMonotonicallyIncreasing(Point2D currentPoint, Point2D previousPoint, int lineNumber) { if (currentPoint.X <= previousPoint.X) { throw CreateCriticalFileReadException(lineNumber, Resources.DikeProfileDataReader_ValidateDikePointsAreMonotonicallyIncreasing_Error_message); } } /// /// Attempts to match the given text to a VOORLAND key-value pair. If a match is /// found, the data block is being read. If valid, the value is stored. /// /// The text of the VOORLAND key-value pair. /// The data to be updated. /// The reader of the file. /// The line number. /// Thrown when /// /// The value after the VOORLAND key does not represent a valid number. /// Any of read the parameters in the following data block is invalid. /// The keyword has already been defined. /// The X-coordinates of the foreshore are not monotonically increasing. /// private bool TryReadForeshoreRoughnessPoints(string text, DikeProfileData data, TextReader reader, ref int lineNumber) { Match foreshoreGeometryMatch = new Regex(@"^VOORLAND(\s+(?.+?)?)?\s*$").Match(text); if (foreshoreGeometryMatch.Success) { ValidateNoPriorParameterDefinition(Keywords.VOORLAND, lineNumber); string readForeshoreCountText = foreshoreGeometryMatch.Groups["foreshoregeometry"].Value; int numberOfElements = ParseNumberOfForeshoreElements(readForeshoreCountText, lineNumber); ValidateForeshorePointCount(numberOfElements, lineNumber); if (numberOfElements == 0) { readKeywords |= Keywords.VOORLAND; return true; } data.ForeshoreGeometry = new RoughnessPoint[numberOfElements]; for (var i = 0; i < numberOfElements; i++) { lineNumber++; text = ReadLineAndHandleIOExceptions(reader, lineNumber); if (text == null) { string message = string.Format(Resources.DikeProfileDataReader_TryReadForeshoreRoughnessPoints_ForeshoreCount_0_does_not_correspond_ExpectedCount_1_, i, numberOfElements); throw CreateCriticalFileReadException(lineNumber, message); } RoughnessPoint roughnessPoint = ReadRoughnessPoint(text, lineNumber); data.ForeshoreGeometry[i] = roughnessPoint; if (i > 0) { ValidateForeshorePointsAreMonotonicallyIncreasing(roughnessPoint.Point, data.ForeshoreGeometry[i - 1].Point, lineNumber); } } readKeywords |= Keywords.VOORLAND; return true; } return false; } /// /// Parses the number of foreshore roughness points from a piece of text. /// /// The line number. /// The text. /// The number of foreshore roughness points. /// Thrown when /// does not represent a number. private int ParseNumberOfForeshoreElements(string readForeshoreCountText, int lineNumber) { try { return int.Parse(readForeshoreCountText, CultureInfo.InvariantCulture); } catch (FormatException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseNumberOfForeshoreElements_ForeshoreCount_0_not_integer, readForeshoreCountText); throw CreateCriticalFileReadException(lineNumber, message, e); } catch (OverflowException e) { string message = string.Format(Resources.DikeProfileDataReader_ParseNumberOfForeshoreElements_ForeshoreCount_0_overflows, readForeshoreCountText); throw CreateCriticalFileReadException(lineNumber, message, e); } } /// /// Validates the number of foreshore points. /// /// The number of elements. /// The line number. /// Thrown when /// is negative. private void ValidateForeshorePointCount(int numberOfElements, int lineNumber) { if (numberOfElements < 0) { string message = string.Format(Resources.DikeProfileDataReader_ReadDikeProfileData_ForeshoreCount_0_cannot_be_negative, numberOfElements); throw CreateCriticalFileReadException(lineNumber, message); } } /// /// Validates that two foreshore points are monotonically increasing. /// /// The current point. /// The previous point. /// The line number. /// Thrown when /// has an X-coordinate before or equal to that of . private void ValidateForeshorePointsAreMonotonicallyIncreasing(Point2D currentPoint, Point2D previousPoint, int lineNumber) { if (currentPoint.X <= previousPoint.X) { throw CreateCriticalFileReadException(lineNumber, Resources.DikeProfileDataReader_ValidateForeshorePointsAreMonotonicallyIncreasing_Error_message); } } private bool TryReadMemo(string text, DikeProfileData data, StreamReader reader) { Match memoMatch = new Regex(@"^MEMO\s*$").Match(text); if (memoMatch.Success) { data.Memo = reader.ReadToEnd(); readKeywords |= Keywords.MEMO; return true; } return false; } /// /// Validates that there is no prior parameter definition of a given parameter. /// /// The parameter. /// The line number. /// Thrown when /// already has been defined in the file at a line prior to . private void ValidateNoPriorParameterDefinition(Keywords parameter, int lineNumber) { if (readKeywords.HasFlag(parameter)) { string message = string.Format(Resources.DikeProfileDataReader_ValidateNoPriorParameterDefinition_Parameter_0_already_defined, parameter); throw CreateCriticalFileReadException(lineNumber, message); } } private static bool CanSafelyCastToEnum(int parameterValue) where TEnum : struct, IConvertible, IComparable, IFormattable { return Enum.GetValues(typeof(TEnum)) .OfType() .Select(dt => Convert.ToInt32(dt)) .Any(i => i.Equals(parameterValue)); } /// /// 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) { string message = new FileReaderErrorMessageBuilder(fileBeingRead).Build(string.Format(UtilsResources.Error_General_IO_ErrorMessage_0_, e.Message)); throw new CriticalFileReadException(message, e); } } /// /// Returns 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); string message = new FileReaderErrorMessageBuilder(fileBeingRead).WithLocation(locationDescription) .Build(criticalErrorMessage); return new CriticalFileReadException(message, innerException); } /// /// Handles the error-case that the file has unexpected text. /// /// The unexpected text. /// The line number. /// Thrown when calling this method, due to /// having read an unexpected piece of text from the file. private void HandleUnexpectedText(string text, int lineNumber) { string message = string.Format(Resources.DikeProfileDataReader_HandleUnexpectedText_Line_0_is_invalid, text); throw CreateCriticalFileReadException(lineNumber, message); } private void ValidateNoMissingKeywords() { string[] missingKeywords = GetMissingKeywords(); if (missingKeywords.Any()) { string criticalErrorMessage = string.Format(Resources.DikeProfileDataReader_ValidateNoMissingKeywords_List_mising_keywords_0_, string.Join(", ", missingKeywords)); string message = new FileReaderErrorMessageBuilder(fileBeingRead) .Build(criticalErrorMessage); throw new CriticalFileReadException(message); } } private string[] GetMissingKeywords() { var requiredKeywords = new[] { Keywords.VERSIE, Keywords.ID, Keywords.RICHTING, Keywords.DAM, Keywords.DAMHOOGTE, Keywords.VOORLAND, Keywords.DAMWAND, Keywords.KRUINHOOGTE, Keywords.DIJK, Keywords.MEMO }; return requiredKeywords.Where(z => !readKeywords.HasFlag(z)) .Select(z => z.ToString()) .ToArray(); } } }