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