diff --git a/Src/WitsmlExplorer.Api/Jobs/DownloadLogDataJob.cs b/Src/WitsmlExplorer.Api/Jobs/DownloadLogDataJob.cs index 8bbca2110..8c3cd4d37 100644 --- a/Src/WitsmlExplorer.Api/Jobs/DownloadLogDataJob.cs +++ b/Src/WitsmlExplorer.Api/Jobs/DownloadLogDataJob.cs @@ -25,6 +25,11 @@ public record DownloadLogDataJob : Job /// public bool StartIndexIsInclusive { get; init; } + /// + /// If to export to LAS format (default is CSV) + /// + public bool ExportToLas { get; init; } + /// /// Start index for range selection /// diff --git a/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs b/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs index 9d1701874..43258b323 100644 --- a/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs +++ b/Src/WitsmlExplorer.Api/Workers/DownloadLogDataWorker.cs @@ -1,11 +1,18 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; using System.Linq; +using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Witsml; +using Witsml.Data; + using WitsmlExplorer.Api.Jobs; using WitsmlExplorer.Api.Models; using WitsmlExplorer.Api.Models.Reports; @@ -20,16 +27,19 @@ public class DownloadLogDataWorker : BaseWorker, IWorker { public JobType JobType => JobType.DownloadLogData; private readonly ILogObjectService _logObjectService; + private readonly IWellService _wellService; private readonly char _newLineCharacter = '\n'; private readonly char _separator = ','; public DownloadLogDataWorker( ILogger logger, IWitsmlClientProvider witsmlClientProvider, - ILogObjectService logObjectService) + ILogObjectService logObjectService, + IWellService wellService) : base(witsmlClientProvider, logger) { _logObjectService = logObjectService; + _wellService = wellService; } /// /// Downaloads all log data and generates a report. @@ -56,19 +66,45 @@ public DownloadLogDataWorker( } var logData = await _logObjectService.ReadLogData(job.LogReference.WellUid, job.LogReference.WellboreUid, job.LogReference.Uid, job.Mnemonics.ToList(), job.StartIndexIsInclusive, job.LogReference.StartIndex, job.LogReference.EndIndex, true, cancellationToken, progressReporter); - - return DownloadLogDataResult(job, logData.Data, logData.CurveSpecifications); + return (job.ExportToLas) + ? await DownloadLogDataResultLasFile(job, logData.Data, + logData.CurveSpecifications) + : DownloadLogDataResultCsvFile(job, logData.Data, + logData.CurveSpecifications); } - private (WorkerResult, RefreshAction) DownloadLogDataResult(DownloadLogDataJob job, ICollection> reportItems, ICollection curveSpecifications) + private (WorkerResult, RefreshAction) DownloadLogDataResultCsvFile(DownloadLogDataJob job, ICollection> reportItems, ICollection curveSpecifications) { - Logger.LogInformation("Download of all data is done. {jobDescription}", job.Description()); + Logger.LogInformation("Download of log data is done. {jobDescription}", job.Description()); string content = GetCsvFileContent(reportItems, curveSpecifications); job.JobInfo.Report = DownloadLogDataReport(job.LogReference, content, "csv"); WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Download of all data is ready, jobId: ", jobId: job.JobInfo.Id); return (workerResult, null); } + private async Task<(WorkerResult, RefreshAction)> DownloadLogDataResultLasFile(DownloadLogDataJob job, ICollection> reportItems, ICollection curveSpecifications) + { + Logger.LogInformation("Download of log data is done. {jobDescription}", job.Description()); + var well = await _wellService.GetWell(job.LogReference.WellUid); + var logObject = await _logObjectService.GetLog(job.LogReference.WellUid, job.LogReference.WellboreUid, job.LogReference.Uid); + var columnLengths = CalculateColumnLength(reportItems, + curveSpecifications); + var maxWellDataLength = CalculateMaxWellDataLength(well, logObject); + var maxHeaderLength = + CalculateMaxHeaderLength(curveSpecifications); + await using var writer = new StringWriter(); + WriteLogCommonInformation(writer, maxHeaderLength, maxWellDataLength); + var limitValues = GetLimitValues(curveSpecifications, reportItems, logObject); + WriteWellInformationSection(writer, well, logObject, maxHeaderLength, maxWellDataLength, limitValues); + WriteLogDefinitionSection(writer, curveSpecifications, maxHeaderLength, maxWellDataLength); + WriteColumnHeaderSection(writer, curveSpecifications, columnLengths); + WriteDataSection(writer, reportItems, curveSpecifications, columnLengths); + string content = writer.ToString(); + job.JobInfo.Report = DownloadLogDataReport(job.LogReference, content, "las"); + WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Download of all data is ready, jobId: ", jobId: job.JobInfo.Id); + return (workerResult, null); + } + private DownloadLogDataReport DownloadLogDataReport(LogObject logReference, string fileContent, string fileExtension) { return new DownloadLogDataReport @@ -114,4 +150,335 @@ private string GetReportBody(ICollection> repor ); return body; } + + private Dictionary CalculateColumnLength(ICollection> data, ICollection curveSpecifications) + { + var result = new Dictionary(); + foreach (var curveSpecification in curveSpecifications) + { + var indexCurveColumn = data.Select(row => + + row.TryGetValue(curveSpecification.Mnemonic, out LogDataValue value) + ? value.Value.ToString()!.Length + : 0 + ).Max(); + result[curveSpecification.Mnemonic] = (curveSpecification.Mnemonic.Length + curveSpecification.Unit.Length + 3) > indexCurveColumn ? curveSpecification.Mnemonic.Length + curveSpecification.Unit.Length + 3 : indexCurveColumn; + } + return result; + } + + private int CalculateMaxWellDataLength(Well well, LogObject logObject) + { + // long date time string, possible the biggest value + var result = 28; + Type objType = typeof(Well); + PropertyInfo[] properties = objType.GetProperties(); + foreach (var property in properties) + { + var value = property.GetValue(well); + if (value != null) + { + if (value.ToString().Length > result) + { + result = value.ToString().Length; + } + } + } + + if (logObject.ServiceCompany != null && logObject.ServiceCompany.Length > result) + result = logObject.ServiceCompany.Length; + return result; + } + + private int CalculateMaxHeaderLength( + ICollection curveSpecifications) + { + var result = 0; + foreach (var curveSpecification in curveSpecifications) + { + if ((curveSpecification.Mnemonic.Length + + curveSpecification.Unit.Length) > result) + result = curveSpecification.Mnemonic.Length + + curveSpecification.Unit.Length; + } + return result; + } + + private LimitValues GetLimitValues(ICollection curveSpecifications, + ICollection> data, LogObject logObject) + { + var curveSpecification = + curveSpecifications.FirstOrDefault(x => string.Equals(x.Mnemonic, logObject.IndexCurve, StringComparison.CurrentCultureIgnoreCase)); + var isDepthBasedSeries = logObject.IndexType == WitsmlLog.WITSML_INDEX_TYPE_MD; + var result = new LimitValues(); + if (curveSpecification == null) + return result; + var indexCurveColumn = isDepthBasedSeries + ? data.Select(row => + + row.TryGetValue(curveSpecification.Mnemonic, out LogDataValue value) + ? value.Value.ToString() + : "0" + ).ToList() + : data.Select(row => + row.TryGetValue(curveSpecification.Mnemonic, out LogDataValue value) + ? value.Value.ToString() + : DateTime.Now.ToString(CultureInfo.InvariantCulture) + ).ToList(); + var firstValue = indexCurveColumn.First(); + result.Start = firstValue; + result.Stop = indexCurveColumn.Last(); + result.Step = CalculateStep(indexCurveColumn, firstValue, isDepthBasedSeries); + result.Unit = curveSpecification.Unit; + result.LogType = isDepthBasedSeries + ? "DEPTH" + : "TIME"; + return result; + } + + private string CalculateStep(List indexCurveColumn, string firstValue, bool isDepthBasedSeries) + { + var result = string.Empty; + foreach (var row in indexCurveColumn) + { + if (firstValue == row) + { + continue; + } + + result = isDepthBasedSeries ? CalculateStepDepth(row, firstValue) : CalculateStepTime(row, firstValue); + if (result == string.Empty) + return result; + firstValue = row; + } + return result; + } + + private string CalculateStepTime(string row, string firstValue) + { + var secondValue = StringHelpers.ToDateTime(row); + var difference = secondValue - + StringHelpers.ToDateTime(firstValue); + var newDifference = StringHelpers.ToDateTime(row) - StringHelpers.ToDateTime(firstValue); + if (difference != newDifference) + { + return string.Empty; + } + return difference.ToString(); + } + + private string CalculateStepDepth(string row, string firstValue) + { + var secondValue = StringHelpers.ToDecimal(row); + var difference = secondValue - + StringHelpers.ToDecimal(firstValue); + var newDifference = StringHelpers.ToDecimal(row) - StringHelpers.ToDecimal(firstValue); + if (difference != newDifference) + { + return string.Empty; + } + return difference.ToString(CultureInfo.InvariantCulture); + } + + private void WriteLogCommonInformation(StringWriter writer, int maxColumnLenght, int maxDataLength) + { + writer.WriteLine("~VERSION INFORMATION"); + WriteCommonParameter(writer, "VERS.", "2.0", "CWLS LOG ASCII STANDARD - VERSION 2.0", maxColumnLenght, maxDataLength); + WriteCommonParameter(writer, "WRAP.", "NO", "ONE LINE PER STEP", maxColumnLenght, maxDataLength); + WriteCommonParameter(writer, "PROD.", "Equinor", "LAS Producer", maxColumnLenght, maxDataLength); + WriteCommonParameter(writer, "PROG.", "WITSML Explorer", "LAS Program name", maxColumnLenght, maxDataLength); + WriteCommonParameter(writer, "CREA.", DateTime.Now.ToShortDateString(), "LAS Creation date", maxColumnLenght, maxDataLength); + } + private void WriteLogDefinitionSection(StringWriter writer, ICollection curveSpecifications, int maxColumnLenght, int maxDataLenght) + { + writer.WriteLine("~PARAMETER INFORMATION"); + writer.WriteLine("~CURVE INFORMATION"); + CreateHeader(writer, maxColumnLenght, maxDataLenght, "#MNEM", ".UNIT", "API CODE", "CURVE DESCRIPTION"); + int i = 1; + foreach (var curveSpecification in curveSpecifications) + { + var line = new StringBuilder(); + line.Append(curveSpecification.Mnemonic); + line.Append(new string(' ', maxColumnLenght - curveSpecification.Mnemonic.Length)); + line.Append($".{curveSpecification.Unit}"); + line.Append(new string(' ', maxColumnLenght - curveSpecification.Unit.Length)); + line.Append(new string(' ', maxDataLenght)); + line.Append($": {i++} "); + line.Append(curveSpecification.Mnemonic.Replace("_", " ")); + line.Append($" ({curveSpecification.Unit})"); + writer.WriteLine(line.ToString()); + } + } + + private void CreateHeader(StringWriter writer, int maxColumnLenght, int maxDataLenght, string firstColumn, string secondColumn, string thirdColumn, string fourthColumn) + { + var header = new StringBuilder(); + var secondHeader = new StringBuilder(); + header.Append(firstColumn); + secondHeader.Append('#'); + secondHeader.Append(new string('-', firstColumn.Length - 1)); + if (maxColumnLenght > firstColumn.Length) + { + header.Append(new string(' ', maxColumnLenght - firstColumn.Length)); + secondHeader.Append(new string(' ', maxColumnLenght - firstColumn.Length)); + } + header.Append(secondColumn); + secondHeader.Append(new string('-', secondColumn.Length)); + if (maxColumnLenght > secondColumn.Length) + { + header.Append(new string(' ', maxColumnLenght - secondColumn.Length)); + secondHeader.Append(new string(' ', maxColumnLenght - secondColumn.Length)); + } + header.Append(thirdColumn); + secondHeader.Append(new string('-', thirdColumn.Length)); + if (maxDataLenght > thirdColumn.Length) + { + header.Append(new string(' ', maxDataLenght - thirdColumn.Length + 1)); + secondHeader.Append(new string(' ', maxDataLenght - thirdColumn.Length + 1)); + } + header.Append(fourthColumn); + secondHeader.Append(new string('-', fourthColumn.Length)); + writer.WriteLine(header.ToString()); + writer.WriteLine(secondHeader.ToString()); + } + + private void WriteColumnHeaderSection(StringWriter writer, ICollection curveSpecifications, Dictionary maxColumnLenghts) + { + writer.WriteLine("#"); + writer.WriteLine( + "#-----------------------------------------------------------"); + int i = 0; + var line = new StringBuilder(); + foreach (var curveSpecification in curveSpecifications) + { + int emptySpaces = maxColumnLenghts[curveSpecification.Mnemonic] - + curveSpecification.Mnemonic.Length - + curveSpecification.Unit.Length - 3; + if (i == 0) + { + line.Append("# "); + if (emptySpaces > 2) + line.Append(new string(' ', emptySpaces - 2)); + } + else + { + if (emptySpaces > 0) + line.Append(new string(' ', emptySpaces)); + } + + line.Append(curveSpecification.Mnemonic); + line.Append(" ("); + line.Append(curveSpecification.Unit); + if (i < curveSpecifications.Count) + line.Append(") "); + else + { + line.Append(')'); + } + i++; + } + writer.WriteLine(line.ToString()); + writer.WriteLine( + "#-----------------------------------------------------------"); + writer.WriteLine("~A"); + } + + private void WriteWellInformationSection(StringWriter writer, Well well, LogObject logObject, int maxColumnLength, int maxDataLenght, LimitValues limitValues) + { + writer.WriteLine("~WELL INFORMATION"); + CreateHeader(writer, maxColumnLength, maxDataLenght, "#MNEM", ".UNIT", "DATA", "DESCRIPTION OF MNEMONIC"); + WriteWellParameter(writer, "STRT", limitValues.Unit, limitValues.Start, $"START {limitValues.LogType}", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "STOP", limitValues.Unit, limitValues.Stop, $"STOP {limitValues.LogType}", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "STEP", limitValues.Unit, limitValues.Step, "STEP VALUE", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "NULL", "", "", "NULL VALUE", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "COMP", "", well.Operator, "COMPANY NAME", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "WELL", "", well.Name, "WELL NAME", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "FLD", "", well.Field, "FIELD NAME", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "SRVC", "", logObject.ServiceCompany, "SERVICE COMPANY NAME", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "DATE", "", $"{DateTime.Now.ToShortDateString()} {DateTime.Now.ToShortTimeString()}", "DATE", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "CTRY", "", well.Country, "COUNTRY", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "UWI", "", well.Uid, "UNIQUE WELL IDENTIFIER", maxColumnLength, maxDataLenght); + WriteWellParameter(writer, "LIC", "", well.NumLicense, "ERCB LICENCE NUMBER", maxColumnLength, maxDataLenght); + } + + private void WriteCommonParameter(StringWriter writer, string nameOfParameter, string data, string description, int maxColumnLength, int maxDataLenght) + { + var line = new StringBuilder(); + line.Append(nameOfParameter); + if (maxColumnLength - nameOfParameter.Length > 0) + { + line.Append(new string(' ', maxColumnLength - nameOfParameter.Length)); + } + line.Append($" {data}"); + if (maxColumnLength - data.Length > 0) + { + line.Append(new string(' ', maxColumnLength - data.Length)); + } + line.Append(new string(' ', maxDataLenght)); + line.Append($":{description}"); + writer.WriteLine(line.ToString()); + } + private void WriteWellParameter(StringWriter writer, string nameOfParemeter, string unit, + string data, string description, int maxColumnLength, int maxDataLength) + { + var line = new StringBuilder(); + line.Append(nameOfParemeter); + if (maxColumnLength - nameOfParemeter.Length > 0) + { + line.Append(new string(' ', maxColumnLength - nameOfParemeter.Length)); + } + line.Append($".{unit}"); + if (maxColumnLength - unit.Length - 1 > 0) + { + line.Append(new string(' ', maxColumnLength - unit.Length - 1)); + } + line.Append(data); + if (data == null) + { + line.Append(new string(' ', maxDataLength)); + } + else if (maxDataLength - data.Length > 0) + { + line.Append(new string(' ', maxDataLength - data.Length)); + } + line.Append($" :{description}"); + writer.WriteLine(line.ToString()); + } + + + private void WriteDataSection(StringWriter writer, + ICollection> data, ICollection curveSpecifications, Dictionary columnsLength) + { + foreach (var row in data) + { + var line = new StringBuilder(); + int i = 0; + foreach (var curveSpecification in curveSpecifications) + { + var cell = row.TryGetValue(curveSpecification.Mnemonic, out LogDataValue value) + ? value.Value.ToString() + : CommonConstants.DepthIndex.NullValue.ToString(CultureInfo.InvariantCulture); + + int length = columnsLength[curveSpecification.Mnemonic] - cell!.Length; + if (i == 0 && length != 0) + { + length += 2; + } + if (length > 0) line.Append(new string(' ', length)); + line.Append(cell); + if (i < curveSpecifications.Count - 1) line.Append(' '); + i++; + } + writer.WriteLine(line.ToString()); + } + } + + private class LimitValues + { + public string Start { get; set; } + public string Stop { get; set; } + public string Step { get; set; } + public string Unit { get; set; } + public string LogType { get; set; } + } } diff --git a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx index b2e754135..e4631b858 100644 --- a/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx +++ b/Src/WitsmlExplorer.Frontend/components/ContentViews/CurveValuesView.tsx @@ -86,6 +86,11 @@ enum DownloadOptions { SelectedIndexValues = "SelectedIndexValues" } +enum DownloadFormat { + Csv = "Csv", + Las = "Las" +} + export const CurveValuesView = (): React.ReactElement => { const { operationState: { timeZone, dateTimeFormat, colors, theme }, @@ -127,6 +132,7 @@ export const CurveValuesView = (): React.ReactElement => { const { exportData, exportOptions } = useExport(); const justFinishedStreaming = useRef(false); let downloadOptions: DownloadOptions = DownloadOptions.SelectedRange; + let downloadFormat: DownloadFormat = DownloadFormat.Csv; const { components: logCurveInfoList, isFetching: isFetchingLogCurveInfo } = useGetComponents( connectedServer, @@ -152,6 +158,14 @@ export const CurveValuesView = (): React.ReactElement => { downloadOptions = enumToString; }; + const onChangeDownloadFormat = ( + event: React.ChangeEvent + ) => { + const selectedValue = event.target.value; + const enumToString = selectedValue as DownloadFormat; + downloadFormat = enumToString; + }; + const onRowSelectionChange = useCallback( (rows: CurveValueRow[]) => setSelectedRows(rows), [] @@ -409,10 +423,12 @@ export const CurveValuesView = (): React.ReactElement => { const exportSelectedRange = async () => { const logReference: LogObject = log; const startIndexIsInclusive = !autoRefresh; + const exportToLas = downloadFormat === DownloadFormat.Las; const downloadLogDataJob: DownloadLogDataJob = { logReference, mnemonics, startIndexIsInclusive, + exportToLas, startIndex, endIndex }; @@ -422,10 +438,12 @@ export const CurveValuesView = (): React.ReactElement => { const exportAll = async () => { const logReference: LogObject = log; const startIndexIsInclusive = !autoRefresh; + const exportToLas = downloadFormat === DownloadFormat.Las; const downloadLogDataJob: DownloadLogDataJob = { logReference, mnemonics, - startIndexIsInclusive + startIndexIsInclusive, + exportToLas }; callExportJob(downloadLogDataJob); }; @@ -484,6 +502,30 @@ export const CurveValuesView = (): React.ReactElement => { /> Download all data + +
+ + Choose file type + + + } onConfirm={() => { diff --git a/Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx b/Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx index 8bf59db81..8e2f15afb 100644 --- a/Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx +++ b/Src/WitsmlExplorer.Frontend/models/jobs/downloadLogDataJob.tsx @@ -4,6 +4,7 @@ export default interface DownloadLogDataJob { logReference: LogObject; mnemonics: string[]; startIndexIsInclusive: boolean; + exportToLas: boolean; startIndex?: string; endIndex?: string; }