Skip to content

Commit

Permalink
Merge pull request #2064 from eliasbruvik/FIX-2057
Browse files Browse the repository at this point in the history
FIX-2057 implement SpliceLogsJob
  • Loading branch information
eliasbruvik authored Oct 5, 2023
2 parents 504b04f + 74dfe6f commit 2d2d8a9
Show file tree
Hide file tree
Showing 10 changed files with 975 additions and 5 deletions.
31 changes: 31 additions & 0 deletions Src/WitsmlExplorer.Api/Jobs/SpliceLogsJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using WitsmlExplorer.Api.Jobs.Common;

namespace WitsmlExplorer.Api.Jobs
{
public record SpliceLogsJob : Job
{
public ObjectReferences Logs { get; init; }
public string NewLogName { get; init; }
public string NewLogUid { get; init; }

public override string Description()
{
return $"Splice logs - WellUid: {Logs.WellUid}; WellboreUid: {Logs.WellboreUid}; Uids: {string.Join(", ", Logs.ObjectUids)};";
}

public override string GetObjectName()
{
return string.Join(", ", Logs.Names);
}

public override string GetWellboreName()
{
return Logs.WellboreName;
}

public override string GetWellName()
{
return Logs.WellName;
}
}
}
3 changes: 2 additions & 1 deletion Src/WitsmlExplorer.Api/Models/JobType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public enum JobType
ReplaceObjects,
CheckLogHeader,
MissingData,
AnalyzeGaps
AnalyzeGaps,
SpliceLogs
}
}
15 changes: 15 additions & 0 deletions Src/WitsmlExplorer.Api/Query/LogQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ public static WitsmlLogs GetWitsmlLogById(string wellUid, string wellboreUid, st
};
}

public static WitsmlLogs GetWitsmlLogsByIds(string wellUid, string wellboreUid, string[] logUids)
{
return new WitsmlLogs
{
Logs = logUids.Select(logUid =>
new WitsmlLog
{
Uid = logUid,
UidWell = wellUid,
UidWellbore = wellboreUid
}
).ToList()
};
}

public static WitsmlLogs GetLogContent(
string wellUid,
string wellboreUid,
Expand Down
224 changes: 224 additions & 0 deletions Src/WitsmlExplorer.Api/Workers/SpliceLogsWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;

using Witsml;
using Witsml.Data;
using Witsml.Extensions;
using Witsml.ServiceReference;

using WitsmlExplorer.Api.Jobs;
using WitsmlExplorer.Api.Models;
using WitsmlExplorer.Api.Query;
using WitsmlExplorer.Api.Services;

namespace WitsmlExplorer.Api.Workers
{
public class SpliceLogsWorker : BaseWorker<SpliceLogsJob>, IWorker
{
public JobType JobType => JobType.SpliceLogs;

public SpliceLogsWorker(ILogger<SpliceLogsJob> logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { }
public override async Task<(WorkerResult, RefreshAction)> Execute(SpliceLogsJob job)
{
string wellUid = job.Logs.WellUid;
string wellboreUid = job.Logs.WellboreUid;
string[] logUids = job.Logs.ObjectUids;
string jobId = job.JobInfo.Id;
string newLogName = job.NewLogName;
string newLogUid = job.NewLogUid;

WitsmlLogs logHeaders = await GetLogHeaders(wellUid, wellboreUid, logUids);
WitsmlLog newLogHeader = CreateNewLogQuery(logHeaders, newLogUid, newLogName);

try
{
VerifyLogHeaders(logHeaders);

bool isDescending = logHeaders.Logs.FirstOrDefault().Direction == WitsmlLog.WITSML_DIRECTION_DECREASING;
bool isDepthLog = logHeaders.Logs.FirstOrDefault().IndexType == WitsmlLog.WITSML_INDEX_TYPE_MD;

WitsmlLogData newLogData = new()
{
MnemonicList = string.Join(",", newLogHeader.LogCurveInfo.Select(lci => lci.Mnemonic)),
UnitList = string.Join(",", newLogHeader.LogCurveInfo.Select(lci => lci.Unit)),
Data = new() // Will be populated in the loop below
};

foreach (string logUid in logUids)
{
WitsmlLog logHeader = logHeaders.Logs.Find(l => l.Uid == logUid);
foreach (var mnemonic in logHeader.LogCurveInfo.Select(lci => lci.Mnemonic).Skip(1))
{
WitsmlLogData logData = await GetLogDataForCurve(logHeader, mnemonic);
newLogData = SpliceLogDataForCurve(newLogData, logData, mnemonic, isDescending, isDepthLog);
}
}

await CreateNewLog(newLogHeader);
await AddDataToLog(wellUid, wellboreUid, newLogUid, newLogData);
}
catch (ArgumentException e)
{
var message = $"SpliceLogsJob failed. Description: {job.Description()}. Error: {e.Message} ";
Logger.LogError(message);
return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, message), null);
}

Logger.LogInformation("{JobType} - Job successful", GetType().Name);

WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Spliced logs: {job.GetObjectName()} to log: {newLogName}", jobId: jobId);
RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), wellUid, wellboreUid, EntityType.Log);
return (workerResult, refreshAction);
}

private async Task<WitsmlLogs> GetLogHeaders(string wellUid, string wellboreUid, string[] logUids)
{
WitsmlLogs logQuery = LogQueries.GetWitsmlLogsByIds(wellUid, wellboreUid, logUids);
return await GetTargetWitsmlClientOrThrow().GetFromStoreAsync(logQuery, new OptionsIn(ReturnElements.HeaderOnly));
}

private static void VerifyLogHeaders(WitsmlLogs logHeaders)
{
if (logHeaders.Logs.IsNullOrEmpty()) throw new ArgumentException("Log headers could not be fetched");
var indexCurve = logHeaders.Logs.FirstOrDefault().IndexCurve;
if (logHeaders.Logs.Any(log => log.IndexCurve.Value != indexCurve.Value)) throw new ArgumentException("IndexCurve must match for all logs");
var direction = logHeaders.Logs.FirstOrDefault().Direction;
if (logHeaders.Logs.Any(log => log.Direction != direction)) throw new ArgumentException("Direction must match for all logs");
var indexType = logHeaders.Logs.FirstOrDefault().IndexType;
if (logHeaders.Logs.Any(log => log.IndexType != indexType)) throw new ArgumentException("Index type must match for all logs");
}

private async Task<WitsmlLogData> GetLogDataForCurve(WitsmlLog log, string mnemonic)
{
await using LogDataReader logDataReader = new(GetTargetWitsmlClientOrThrow(), log, mnemonic.AsSingletonList(), Logger);
List<WitsmlData> data = new();
WitsmlLogData logData = await logDataReader.GetNextBatch();
var mnemonicList = logData?.MnemonicList;
var unitList = logData?.UnitList;
while (logData != null)
{
data.AddRange(logData.Data);
logData = await logDataReader.GetNextBatch();
}

return new WitsmlLogData
{
MnemonicList = mnemonicList,
UnitList = unitList,
Data = data
};
}

private static WitsmlLogData SpliceLogDataForCurve(WitsmlLogData primaryData, WitsmlLogData secondaryData, string mnemonic, bool isDescending, bool isDepthLog)
{
int mnemonicIndex = primaryData.MnemonicList.Split(',').ToList().FindIndex(m => m == mnemonic);
Dictionary<string, string> primaryDict = primaryData.Data?.ToDictionary(row => row.Data.Split(',')[0], row => row.Data) ?? new();
string startIndex = null;
string endIndex = null;
if (primaryDict.Any())
{
var firstElementForCurve = primaryDict.FirstOrDefault(x => x.Value.Split(',')[mnemonicIndex] != "");
startIndex = firstElementForCurve.Equals(default(KeyValuePair<string, string>)) ? null : firstElementForCurve.Key;
var lastElementForCurve = primaryDict.LastOrDefault(x => x.Value.Split(',')[mnemonicIndex] != "");
endIndex = lastElementForCurve.Equals(default(KeyValuePair<string, string>)) ? null : lastElementForCurve.Key;
}

foreach (var dataRow in secondaryData.Data.Select(row => row.Data))
{
var rowIndex = dataRow.Split(',').First();
if ((startIndex == null && endIndex == null)
|| isDepthLog && (double.Parse(rowIndex) < double.Parse(startIndex) || double.Parse(rowIndex) > double.Parse(endIndex))
|| !isDepthLog && (DateTime.Parse(rowIndex) < DateTime.Parse(startIndex) || DateTime.Parse(rowIndex) > DateTime.Parse(endIndex)))
{
var newCellValue = dataRow.Split(',').Last();
var currentRowValue = (primaryDict.GetValueOrDefault(rowIndex)?.Split(',') ?? Enumerable.Repeat("", primaryData.MnemonicList.Split(',').Length)).ToList();
currentRowValue[0] = rowIndex;
currentRowValue[mnemonicIndex] = newCellValue;
primaryDict[rowIndex] = string.Join(",", currentRowValue);
}
}

var sorted = isDepthLog ? primaryDict.OrderBy(x => double.Parse(x.Key)) : primaryDict.OrderBy(x => DateTime.Parse(x.Key));
List<WitsmlData> splicedData = sorted.Select(x => new WitsmlData { Data = x.Value }).ToList();

WitsmlLogData newData = new()
{
MnemonicList = primaryData.MnemonicList,
UnitList = primaryData.UnitList,
Data = splicedData
};

return newData;
}

private async Task CreateNewLog(WitsmlLog newLogHeader)
{
WitsmlLogs query = new()
{
Logs = newLogHeader.AsSingletonList()
};
QueryResult result = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(query);
if (!result.IsSuccessful) throw new ArgumentException($"Could not create log. {result.Reason}");
}

private static WitsmlLog CreateNewLogQuery(WitsmlLogs logHeaders, string newLogUid, string newLogName)
{
WitsmlLog baseLog = logHeaders.Logs.FirstOrDefault();
return new()
{
Name = newLogName,
NameWell = baseLog.NameWell,
NameWellbore = baseLog.NameWellbore,
Uid = newLogUid,
UidWell = baseLog.UidWell,
UidWellbore = baseLog.UidWellbore,
IndexType = baseLog.IndexType,
IndexCurve = baseLog.IndexCurve,
Direction = baseLog.Direction,
LogCurveInfo = logHeaders.Logs
.SelectMany(log => log.LogCurveInfo)
.GroupBy(x => x.Mnemonic)
.Select(g => g.Last())
.ToList()
};
}

private async Task AddDataToLog(string wellUid, string wellboreUid, string logUid, WitsmlLogData data)
{
var batchSize = 5000; // Use maxDataNodes and maxDataPoints to calculate batchSize when supported by the API.
var dataRows = data.Data;
for (int i = 0; i < dataRows.Count; i += batchSize)
{
var currentLogData = dataRows.Skip(i).Take(batchSize).ToList();
WitsmlLogs copyNewCurvesQuery = CreateAddLogDataRowsQuery(wellUid, wellboreUid, logUid, data, currentLogData);
QueryResult result = await RequestUtils.WithRetry(async () => await GetTargetWitsmlClientOrThrow().UpdateInStoreAsync(copyNewCurvesQuery), Logger);
if (!result.IsSuccessful) throw new ArgumentException($"Could not add log data to the new log. {result.Reason}");
}
}

private static WitsmlLogs CreateAddLogDataRowsQuery(string wellUid, string wellboreUid, string logUid, WitsmlLogData logData, List<WitsmlData> currentLogData)
{
return new()
{
Logs = new List<WitsmlLog> {
new(){
UidWell = wellUid,
UidWellbore = wellboreUid,
Uid = logUid,
LogData = new WitsmlLogData
{
MnemonicList = logData.MnemonicList,
UnitList = logData.UnitList,
Data = currentLogData
}
}
}
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const OrderingLabel = styled(Typography)`
font-size: 0.875rem;
`;

const DummyDrop = styled.div<{ isDraggedOver?: number; colors: Colors }>`
export const DummyDrop = styled.div<{ isDraggedOver?: number; colors: Colors }>`
border-top: 2px solid ${(props) => props.colors.ui.backgroundLight};
${(props) =>
props.isDraggedOver
Expand All @@ -186,7 +186,7 @@ const DummyDrop = styled.div<{ isDraggedOver?: number; colors: Colors }>`
: ""}
`;

const Draggable = styled.div<{ isDragged?: number; isDraggedOver?: number; draggingStarted?: number; colors: Colors }>`
export const Draggable = styled.div<{ isDragged?: number; isDraggedOver?: number; draggingStarted?: number; colors: Colors }>`
cursor: grab;
user-select: none;
height: 100%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,20 @@ import { Server } from "../../models/server";
import JobService, { JobType } from "../../services/jobService";
import ObjectService from "../../services/objectService";
import { colors } from "../../styles/Colors";
import AnalyzeGapModal, { AnalyzeGapModalProps } from "../Modals/AnalyzeGapModal";
import LogComparisonModal, { LogComparisonModalProps } from "../Modals/LogComparisonModal";
import LogDataImportModal, { LogDataImportModalProps } from "../Modals/LogDataImportModal";
import LogPropertiesModal from "../Modals/LogPropertiesModal";
import { PropertiesModalMode } from "../Modals/ModalParts";
import ObjectPickerModal, { ObjectPickerProps } from "../Modals/ObjectPickerModal";
import { ReportModal } from "../Modals/ReportModal";
import SpliceLogsModal from "../Modals/SpliceLogsModal";
import TrimLogObjectModal, { TrimLogObjectModalProps } from "../Modals/TrimLogObject/TrimLogObjectModal";
import ContextMenu from "./ContextMenu";
import { StyledIcon, menuItemText } from "./ContextMenuUtils";
import { onClickPaste } from "./CopyUtils";
import { ObjectContextMenuProps, ObjectMenuItems } from "./ObjectMenuItems";
import { useClipboardComponentReferencesOfType } from "./UseClipboardComponentReferences";
import AnalyzeGapModal, { AnalyzeGapModalProps } from "../Modals/AnalyzeGapModal";

const LogObjectContextMenu = (props: ObjectContextMenuProps): React.ReactElement => {
const { checkedObjects, wellbore } = props;
Expand Down Expand Up @@ -110,6 +111,14 @@ const LogObjectContextMenu = (props: ObjectContextMenuProps): React.ReactElement
}
};

const onClickSplice = () => {
dispatchOperation({ type: OperationType.HideContextMenu });
dispatchOperation({
type: OperationType.DisplayModal,
payload: <SpliceLogsModal checkedLogs={checkedObjects} />
});
};

return (
<ContextMenu
menuItems={[
Expand Down Expand Up @@ -146,6 +155,10 @@ const LogObjectContextMenu = (props: ObjectContextMenuProps): React.ReactElement
<StyledIcon name="compare" color={colors.interactive.primaryResting} />
<Typography color={"primary"}>{`${menuItemText("check", "log header", [])}`}</Typography>
</MenuItem>,
<MenuItem key={"splice"} onClick={onClickSplice} disabled={checkedObjects.length < 2}>
<StyledIcon name="compare" color={colors.interactive.primaryResting} />
<Typography color={"primary"}>Splice logs</Typography>
</MenuItem>,
<Divider key={"divider"} />,
<MenuItem key={"properties"} onClick={onClickProperties} disabled={checkedObjects.length !== 1}>
<StyledIcon name="settings" color={colors.interactive.primaryResting} />
Expand Down
Loading

0 comments on commit 2d2d8a9

Please sign in to comment.