// Copyright (C) Stichting Deltares 2025. All rights reserved.
//
// This file is part of the application DAM - UI.
//
// DAM - UI 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.Diagnostics;
using System.IO;
using System.Linq;
using Deltares.Dam.Data.CsvImporters;
using Deltares.Dam.Data.DataPlugins.Configuration;
using Deltares.DamEngine.Data.Standard.Calculation;
using Deltares.Maps;
using NetTopologySuite.Geometries;
namespace Deltares.Dam.Data.Importers;
public class LocationPropertyImporterException : Exception
{
public LocationPropertyImporterException(string message)
: base(message) {}
}
// ReSharper disable ClassWithVirtualMembersNeverInherited.Global
public class LocationPropertyImporter
// ReSharper restore ClassWithVirtualMembersNeverInherited.Global
{
private readonly IDictionary> filesToImportFrom;
private readonly PolygonAttributeImporter polygonAttributeImporter;
private readonly LineAttributeImporter lineAttributeImporter;
private IEnumerable targets;
public LocationPropertyImporter()
{
filesToImportFrom = new Dictionary>();
polygonAttributeImporter = new PolygonAttributeImporter
{
XGetter = t => t.GeoX.Value,
YGetter = t => t.GeoY.Value
};
lineAttributeImporter = new LineAttributeImporter();
}
public LocationPropertyImporter(string dataLocation, IEnumerable dataAttributes) : this()
{
DataFileLocation = dataLocation;
ImproperAttributeMessages.Clear();
// Register configured property attributes
foreach (DataAttribute dataAttribute in dataAttributes)
{
if (LocationShapeFileAttributeMap.IsPropertyAttribute(dataAttribute.AttributeId))
{
RegisterAttributeMapping(dataAttribute.AttributeId, dataAttribute.AttributeName, dataAttribute.DataSource);
}
}
if (ImproperAttributeMessages.Count > 0)
{
string message = Environment.NewLine + "The following attribute id's are supported:" + Environment.NewLine +
string.Join(", ", LocationShapeFileAttributeMap.AttributePropertyMap.Keys.OrderBy(k => k).ToArray());
message += Environment.NewLine + "Please change the id in the shape file or remove the shape for this attribute from the defx.";
ImproperAttributeMessages.Add(message);
}
// Should throw an exception when more then one crosssection is defined..
RegisterCrossSectionAttribute(dataAttributes);
}
public List ImproperAttributeMessages { get; } = new List();
public string DataFileLocation { private get; set; }
public IEnumerable ImportErrors
{
get
{
return polygonAttributeImporter.Errors.Concat(lineAttributeImporter.Errors);
}
}
public IEnumerable Targets
{
set
{
targets = value;
}
}
///
///
///
///
///
public void RegisterCrossSectionAttribute(string customAttributeName, string customFileName)
{
if (!IsValidString(customAttributeName))
{
throw new ArgumentException("customAttributeName");
}
if (!IsValidString(customFileName))
{
throw new ArgumentException("customFileName");
}
ShapeFileLocation file = GetFile(customFileName);
lineAttributeImporter.CrossSectionRepository = GetCrossSectionRepository(file);
lineAttributeImporter.LocationIDAttributeName = GetAttributeName(customAttributeName);
}
// method is used for test purpose
// a cleaner class design should not have this method public
// todo: fix this
public virtual string GetAttributeName(string name)
{
return name;
}
// method is used for test purpose
// a cleaner class design should not have this method public
// todo: fix this
public virtual IFeatureRepository GetCrossSectionRepository(ShapeFileLocation file)
{
return FeatureRepository.CreateFromShapeFile(file.FullPath);
}
///
/// Registers a shape file attribute/column mapping to local id's. The attribute names are set to default names.
///
/// Required argument. Use this argument to refer to a known attribute in the set of supported attributes (declared in this class). For example: TRAFLOAD.
/// The name of the Shape file that contains the data attribute
/// When the attribute id is unknown (not in the set declared here) this exception is thrown
/// When the attribute id is null or empty this exception is thrown
public void RegisterAttributeMapping(string attributeId, string dataFileName)
{
RegisterAttributeMapping(attributeId, null, dataFileName);
}
///
/// Registers a shape file attribute to local id's
///
///
/// The Shape file attributes/columns are in the Dbf file
///
/// Required argument. Use this argument to refer to a known attribute in the set of supported attributes (declared in this class). For example: TRAFLOAD.
/// The name of the attribute in the Shape file to map to. For example TRAFLOAD can be mapped to the TrafficLoad column in the Shape file. If the name is null or Empty the default name will be used. For TRAFLOAD this will be TRAFLOAD.
/// The name of the Shape file that contains the data attribute
/// If the attribute id is unknown (not in the set declared here) this exception is thrown
/// If the attribute id is null or empty this exception is thrown
/// If the file is not found this exceptions will be thrown
public void RegisterAttributeMapping(string attributeId, string customAttributeName, string dataFileName)
{
if (!IsValidString(attributeId))
{
throw new ArgumentException("attributeId");
}
if (!LocationShapeFileAttributeMap.IsAttributeIdSupported(attributeId))
{
var message = $"The attribute id '{attributeId}' is not supported by this importer.";
ImproperAttributeMessages.Add(message);
}
else
{
LocationAttributeMapping mapping = LocationShapeFileAttributeMap.GetAttributeMapping(attributeId);
string attrName = string.IsNullOrEmpty(customAttributeName) || customAttributeName.Trim() == ""
? mapping.Name // return the default shape file colum name
: customAttributeName;
if (!IsValidString(dataFileName)) // not a valid file location? use the default file
{
dataFileName = mapping.File;
}
Action action = mapping.Action;
// Build a map to data (shape) files and attributes contained in the dbf file
// if a file is already added to the map a new attribute will be added
// to support the concept that multiple attributes can be contained in a single (shape) file
// ie. { somedatashapefile.shp, { TRAFFLOAD, PENLENGTH, .. } }
ShapeFileLocation file = GetFile(dataFileName);
string key = file.FullPath;
if (!filesToImportFrom.ContainsKey(key))
{
filesToImportFrom.Add(key, new List());
}
filesToImportFrom[key].Add(new LocationAttributeMapping
{
Action = action,
Name = attrName
});
}
}
// used for testing purposes...
public virtual ShapeFileLocation GetFile(string dataFileName)
{
var file = new ShapeFileLocation(DataFileLocation, dataFileName);
if (!file.Exists)
{
throw new FileNotFoundException($"The file '{file.FullPath}' is not found");
}
return file;
}
// used for testing purposes...
// ReSharper disable MemberCanBeProtected.Global
public virtual IFeatureRepository CreateFromShapeFile(string fileName)
// ReSharper restore MemberCanBeProtected.Global
{
return FeatureRepository.CreateFromShapeFile(new ShapeFileLocation(fileName));
}
///
/// Imports the specified progress.
///
/// The progress.
/// No target locations set
///
///
/// The file " + fileName + " does not exist so can not be imported.
/// or
/// Had problems reading attribute values from file: " + fileName
///
public void Import(ProgressDelegate progress)
{
if (targets == null)
{
throw new InvalidOperationException("No target locations set");
}
try
{
polygonAttributeImporter.Targets = targets;
lineAttributeImporter.Targets = targets;
}
catch (LineAttributeImporterException e)
{
throw new LocationPropertyImporterException($"Error finding locations in crossections, {e.Message}");
}
#if DEBUG
int targetCount = targets.Count();
int crossSectionCount = lineAttributeImporter != null && lineAttributeImporter.CrossSectionRepository != null ? lineAttributeImporter.CrossSectionRepository.Count : 0;
#endif
// use the file / name attribute mapping table to retrieve the
// the actual value from the data (shape) file
double progressStep = 0.6 * (1.0 / filesToImportFrom.Count);
var curProgress = 0.30;
foreach (KeyValuePair> file in filesToImportFrom)
{
string fileName = file.Key;
if (!File.Exists(fileName))
{
throw new ImportException("The file " + fileName + " does not exist so can not be imported.");
}
#if DEBUG
Debug.WriteLine(new string('=', 80));
Debug.WriteLine(fileName);
var stopwatch = new Stopwatch();
stopwatch.Start();
#endif
IFeatureRepository attributeRepository = CreateFromShapeFile(fileName);
IFeature feature = attributeRepository.Features.FirstOrDefault();
var isPolygonTypeRepository = false;
if (feature != null)
{
isPolygonTypeRepository = feature.Geometry is Polygon || feature.Geometry is MultiPolygon;
IAttributeImporter importer = isPolygonTypeRepository ? polygonAttributeImporter : lineAttributeImporter;
importer.FileName = fileName;
importer.AttributeRepository = attributeRepository;
importer.AttributeMappings = file.Value.OfType>();
try
{
importer.Import();
}
catch (AttributeMissingException attributeMissingException)
{
importer.AddError(attributeMissingException);
}
catch (Exception e)
{
throw new ImportException("Had problems reading attribute values from file: " + fileName, e);
}
}
curProgress += progressStep;
if (progress != null)
{
progress(curProgress);
}
#if DEBUG
stopwatch.Stop();
Debug.WriteLine("Number of source features: {0}", isPolygonTypeRepository ? targetCount : crossSectionCount);
Debug.WriteLine("Number of target features: {0}", attributeRepository.Count);
Debug.WriteLine("Time elapsed: {0}s", stopwatch.Elapsed.TotalSeconds);
#endif
}
}
// for now this method is only used for test purposes
internal void RegisterAttributeMapping(string attributeId)
{
RegisterAttributeMapping(attributeId, null);
}
private void RegisterCrossSectionAttribute(IEnumerable dataAttributes)
{
DataAttribute crossSectionConfig = dataAttributes.SingleOrDefault(a => a.AttributeId.Equals(LocationShapeFileAttributeMap.CrossSectionAttributId, StringComparison.InvariantCultureIgnoreCase));
LocationAttributeMapping mapping = crossSectionConfig != null ? LocationShapeFileAttributeMap.GetAttributeMapping(crossSectionConfig) : LocationShapeFileAttributeMap.GetAttributeMapping(LocationShapeFileAttributeMap.CrossSectionAttributId);
RegisterCrossSectionAttribute(mapping.Name, mapping.File);
}
private static bool IsValidString(string value)
{
return !(string.IsNullOrEmpty(value) || value.Trim() == string.Empty);
}
}