diff --git a/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs b/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs
new file mode 100644
index 000000000..2ba40901a
--- /dev/null
+++ b/Src/WitsmlExplorer.Api/Jobs/CreateTrajectoryJob.cs
@@ -0,0 +1,50 @@
+using WitsmlExplorer.Api.Models;
+
+namespace WitsmlExplorer.Api.Jobs;
+
+///
+/// Job for create trajectory with jobInfo.
+///
+public record CreateTrajectoryJob : Job
+{
+ ///
+ /// Trajectory API model.
+ ///
+ public Trajectory Trajectory { get; init; }
+
+ ///
+ /// Getting description of created trajectory.
+ ///
+ /// String of job info which provide Trajectory Uid and Name, WellUid, WellboreUid.
+ public override string Description()
+ {
+ return $"Create Trajectory - Uid: {Trajectory.Uid}; Name: {Trajectory.Name}; WellUid: {Trajectory.WellUid}; WellboreUid: {Trajectory.WellboreUid};";
+ }
+
+ ///
+ /// Getting name of trajectory.
+ ///
+ /// String of trajectory name.
+ public override string GetObjectName()
+ {
+ return Trajectory.Name;
+ }
+
+ ///
+ /// Getting name of wellbore.
+ ///
+ /// String of wellbore name.
+ public override string GetWellboreName()
+ {
+ return Trajectory.WellboreName;
+ }
+
+ ///
+ /// Getting name of well.
+ ///
+ /// String of well name.
+ public override string GetWellName()
+ {
+ return Trajectory.WellName;
+ }
+}
diff --git a/Src/WitsmlExplorer.Api/Models/JobType.cs b/Src/WitsmlExplorer.Api/Models/JobType.cs
index d8c65a4ed..466bfce0d 100644
--- a/Src/WitsmlExplorer.Api/Models/JobType.cs
+++ b/Src/WitsmlExplorer.Api/Models/JobType.cs
@@ -37,6 +37,7 @@ public enum JobType
CreateRisk,
CreateMudLog,
CreateRig,
+ CreateTrajectory,
CreateWbGeometry,
BatchModifyWell,
ImportLogData,
diff --git a/Src/WitsmlExplorer.Api/Models/Trajectory.cs b/Src/WitsmlExplorer.Api/Models/Trajectory.cs
index 313220934..da8eb9bbf 100644
--- a/Src/WitsmlExplorer.Api/Models/Trajectory.cs
+++ b/Src/WitsmlExplorer.Api/Models/Trajectory.cs
@@ -6,14 +6,14 @@ namespace WitsmlExplorer.Api.Models
{
public class Trajectory : ObjectOnWellbore
{
- public decimal? MdMin { get; internal init; }
- public decimal? MdMax { get; internal init; }
- public string AziRef { get; internal init; }
- public string DTimTrajStart { get; internal init; }
- public string DTimTrajEnd { get; internal init; }
- public List TrajectoryStations { get; internal init; }
- public string ServiceCompany { get; internal init; }
- public string DateTimeCreation { get; internal init; }
- public string DateTimeLastChange { get; internal init; }
+ public decimal? MdMin { get; init; }
+ public decimal? MdMax { get; init; }
+ public string AziRef { get; init; }
+ public string DTimTrajStart { get; init; }
+ public string DTimTrajEnd { get; init; }
+ public List TrajectoryStations { get; init; }
+ public string ServiceCompany { get; init; }
+ public string DateTimeCreation { get; init; }
+ public string DateTimeLastChange { get; init; }
}
}
diff --git a/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs b/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs
index 87afdf3f1..dfb83295a 100644
--- a/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs
+++ b/Src/WitsmlExplorer.Api/Query/TrajectoryQueries.cs
@@ -90,6 +90,34 @@ public static WitsmlTrajectories DeleteTrajectoryStations(string wellUid, string
}.AsSingletonList()
};
}
+
+ ///
+ /// Create trajectories witsml model.
+ ///
+ /// API model of trajectory data.
+ /// New instance of WitsmlTrajectories model with added trajectory data.
+ public static WitsmlTrajectories CreateTrajectory(Trajectory trajectory)
+ {
+ return new()
+ {
+ Trajectories = new WitsmlTrajectory
+ {
+ UidWell = trajectory.WellUid,
+ NameWell = trajectory.WellName,
+ NameWellbore = trajectory.WellboreName,
+ Uid = trajectory.Uid,
+ Name = trajectory.Name,
+ UidWellbore = trajectory.WellboreUid,
+ MdMin = trajectory.MdMin != null ? new WitsmlMeasuredDepthCoord() { Value = trajectory.MdMin.Value.ToString(CultureInfo.InvariantCulture) } : null,
+ MdMax = trajectory.MdMax != null ? new WitsmlMeasuredDepthCoord() { Value = trajectory.MdMax.Value.ToString(CultureInfo.InvariantCulture) } : null,
+ AziRef = trajectory.AziRef.NullIfEmpty(),
+ ServiceCompany = trajectory.ServiceCompany.NullIfEmpty(),
+ DTimTrajStart = StringHelpers.ToUniversalDateTimeString(trajectory.DTimTrajStart),
+ DTimTrajEnd = StringHelpers.ToUniversalDateTimeString(trajectory.DTimTrajEnd),
+ }.AsSingletonList()
+ };
+ }
+
public static WitsmlTrajectories UpdateTrajectoryStation(TrajectoryStation trajectoryStation, ObjectReference trajectoryReference)
{
WitsmlTrajectoryStation ts = new()
diff --git a/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs b/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs
new file mode 100644
index 000000000..a10481a6f
--- /dev/null
+++ b/Src/WitsmlExplorer.Api/Workers/Create/CreateTrajectoryWorker.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Logging;
+
+using Witsml;
+using Witsml.Data;
+
+using WitsmlExplorer.Api.Jobs;
+using WitsmlExplorer.Api.Models;
+using WitsmlExplorer.Api.Query;
+using WitsmlExplorer.Api.Services;
+
+namespace WitsmlExplorer.Api.Workers.Create;
+
+///
+/// Worker for creating new trajectory by specific well and wellbore.
+///
+public class CreateTrajectoryWorker : BaseWorker, IWorker
+{
+ public CreateTrajectoryWorker(ILogger logger, IWitsmlClientProvider witsmlClientProvider) : base(witsmlClientProvider, logger) { }
+ public JobType JobType => JobType.CreateTrajectory;
+
+ ///
+ /// Create new trajectory on wellbore for witsml client.
+ ///
+ /// Job info of created trajectory.
+ /// Task of workerResult with refresh objects.
+ public override async Task<(WorkerResult, RefreshAction)> Execute(CreateTrajectoryJob job)
+ {
+ Verify(job.Trajectory);
+
+ WitsmlTrajectories trajectoryToCreate = TrajectoryQueries.CreateTrajectory(job.Trajectory);
+
+ QueryResult addToStoreResult = await GetTargetWitsmlClientOrThrow().AddToStoreAsync(trajectoryToCreate);
+
+ if (!addToStoreResult.IsSuccessful)
+ {
+ string errorMessage = "Failed to create trajectory.";
+ Logger.LogError("{ErrorMessage}. {jobDescription}", errorMessage, job.Description());
+ return (new WorkerResult(GetTargetWitsmlClientOrThrow().GetServerHostname(), false, errorMessage, addToStoreResult.Reason), null);
+ }
+
+ Logger.LogInformation("Trajectory created. {jobDescription}", job.Description());
+ RefreshObjects refreshAction = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), job.Trajectory.WellUid, job.Trajectory.WellboreUid, EntityType.Trajectory);
+ WorkerResult workerResult = new(GetTargetWitsmlClientOrThrow().GetServerHostname(), true, $"Trajectory {job.Trajectory.Name} add for {job.Trajectory.WellboreName}");
+
+ return (workerResult, refreshAction);
+ }
+
+ private static void Verify(Trajectory trajectory)
+ {
+ if (string.IsNullOrEmpty(trajectory.Uid))
+ {
+ throw new InvalidOperationException($"{nameof(trajectory.Uid)} cannot be empty");
+ }
+
+ if (string.IsNullOrEmpty(trajectory.Name))
+ {
+ throw new InvalidOperationException($"{nameof(trajectory.Name)} cannot be empty");
+ }
+ }
+}
diff --git a/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx
new file mode 100644
index 000000000..1a73bd356
--- /dev/null
+++ b/Src/WitsmlExplorer.Frontend/components/ContextMenus/TrajectoriesContextMenu.tsx
@@ -0,0 +1,79 @@
+import { Typography } from "@equinor/eds-core-react";
+import { MenuItem } from "@material-ui/core";
+import React, { useContext } from "react";
+import { v4 as uuid } from "uuid";
+import { DisplayModalAction, HideContextMenuAction, HideModalAction } from "../../contexts/operationStateReducer";
+import OperationType from "../../contexts/operationType";
+import Wellbore from "../../models/wellbore";
+import { colors } from "../../styles/Colors";
+import { PropertiesModalMode } from "../Modals/ModalParts";
+import ContextMenu from "./ContextMenu";
+import { menuItemText, onClickRefresh, StyledIcon } from "./ContextMenuUtils";
+import TrajectoryPropertiesModal, { TrajectoryPropertiesModalProps } from "../Modals/TrajectoryPropertiesModal";
+import { ObjectType } from "../../models/objectType";
+import { pasteObjectOnWellbore } from "./CopyUtils";
+import { useClipboardReferencesOfType } from "./UseClipboardReferences";
+import NavigationContext from "../../contexts/navigationContext";
+import { Server } from "../../models/server";
+import Trajectory from "../../models/trajectory";
+
+export interface TrajectoriesContextMenuProps {
+ dispatchOperation: (action: DisplayModalAction | HideModalAction | HideContextMenuAction) => void;
+ wellbore: Wellbore;
+ servers: Server[];
+ setIsLoading?: (arg: boolean) => void;
+}
+
+const TrajectoriesContextMenu = (props: TrajectoriesContextMenuProps): React.ReactElement => {
+ const { dispatchOperation, wellbore, servers, setIsLoading } = props;
+ const { dispatchNavigation } = useContext(NavigationContext);
+ const trajectoryReferences = useClipboardReferencesOfType(ObjectType.Trajectory);
+
+ const onClickNewTrajectory = () => {
+ const newTrajectory: Trajectory = {
+ uid: uuid(),
+ name: "",
+ wellUid: wellbore.wellUid,
+ wellName: wellbore.wellName,
+ wellboreUid: wellbore.uid,
+ wellboreName: wellbore.name,
+ serviceCompany: "",
+ aziRef: "",
+ dTimTrajEnd: "",
+ dTimTrajStart: "",
+ mdMax: null,
+ mdMin: null,
+ trajectoryStations: []
+ };
+ const trajectoryPropertiesModalProps: TrajectoryPropertiesModalProps = { mode: PropertiesModalMode.New, trajectory: newTrajectory, dispatchOperation };
+ const action: DisplayModalAction = { type: OperationType.DisplayModal, payload: };
+ dispatchOperation(action);
+ };
+
+ return (
+ onClickRefresh(dispatchOperation, dispatchNavigation, wellbore.wellUid, wellbore.uid, ObjectType.Trajectory, setIsLoading)}>
+
+ {`Refresh Trajectories`}
+
+ ) : null,
+ ,
+
+ ]}
+ />
+ );
+};
+
+export default TrajectoriesContextMenu;
diff --git a/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx
new file mode 100644
index 000000000..c4e963993
--- /dev/null
+++ b/Src/WitsmlExplorer.Frontend/components/Modals/TrajectoryPropertiesModal.tsx
@@ -0,0 +1,155 @@
+import { TextField } from "@material-ui/core";
+import React, { useContext, useEffect, useState } from "react";
+import { HideModalAction } from "../../contexts/operationStateReducer";
+import OperationType from "../../contexts/operationType";
+import JobService, { JobType } from "../../services/jobService";
+import ModalDialog from "./ModalDialog";
+import { PropertiesModalMode, validText } from "./ModalParts";
+import Trajectory from "../../models/trajectory";
+import { DateTimeField } from "./DateTimeField";
+import OperationContext from "../../contexts/operationContext";
+export interface TrajectoryPropertiesModalProps {
+ mode: PropertiesModalMode;
+ trajectory: Trajectory;
+ dispatchOperation: (action: HideModalAction) => void;
+}
+
+const TrajectoryPropertiesModal = (props: TrajectoryPropertiesModalProps): React.ReactElement => {
+ const { mode, trajectory, dispatchOperation } = props;
+ const {
+ operationState: { timeZone }
+ } = useContext(OperationContext);
+ const [editableTrajectory, setEditableTrajectory] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [, setDTimTrajStartValid] = useState(true);
+ const [, setDTimTrajEndValid] = useState(true);
+ const editMode = mode === PropertiesModalMode.Edit;
+
+ useEffect(() => {
+ setEditableTrajectory({
+ ...trajectory
+ });
+ }, [trajectory]);
+
+ const onSubmit = async (updatedTrajectory: Trajectory) => {
+ setIsLoading(true);
+ const wellboreTrajectoryJob = {
+ trajectory: updatedTrajectory
+ };
+ await JobService.orderJob(editMode ? JobType.ModifyTrajectory : JobType.CreateTrajectory, wellboreTrajectoryJob);
+ setIsLoading(false);
+ dispatchOperation({ type: OperationType.HideModal });
+ };
+
+ return (
+ <>
+ {editableTrajectory && (
+
+ setEditableTrajectory({ ...editableTrajectory, uid: e.target.value })}
+ />
+
+
+
+
+ setEditableTrajectory({ ...editableTrajectory, name: e.target.value })}
+ />
+ {
+ setEditableTrajectory({ ...editableTrajectory, dTimTrajStart: dateTime });
+ setDTimTrajStartValid(valid);
+ }}
+ timeZone={timeZone}
+ />
+ {
+ setEditableTrajectory({ ...editableTrajectory, dTimTrajEnd: dateTime });
+ setDTimTrajEndValid(valid);
+ }}
+ timeZone={timeZone}
+ />
+ setEditableTrajectory({ ...editableTrajectory, serviceCompany: e.target.value })}
+ />
+
+ setEditableTrajectory({
+ ...editableTrajectory,
+ mdMin: isNaN(parseFloat(e.target.value)) ? undefined : parseFloat(e.target.value)
+ })
+ }
+ />
+
+ setEditableTrajectory({
+ ...editableTrajectory,
+ mdMax: isNaN(parseFloat(e.target.value)) ? undefined : parseFloat(e.target.value)
+ })
+ }
+ />
+ setEditableTrajectory({ ...editableTrajectory, aziRef: e.target.value })}
+ />
+ >
+ }
+ confirmDisabled={!validText(editableTrajectory.uid) || !validText(editableTrajectory.name)}
+ onSubmit={() => onSubmit(editableTrajectory)}
+ isLoading={isLoading}
+ />
+ )}
+ >
+ );
+};
+
+export default TrajectoryPropertiesModal;
diff --git a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx
index e9e0ddf9e..5d9334c3f 100644
--- a/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx
+++ b/Src/WitsmlExplorer.Frontend/components/Sidebar/WellboreItem.tsx
@@ -22,6 +22,7 @@ import TubularContextMenu from "../ContextMenus/TubularContextMenu";
import TubularsContextMenu, { TubularsContextMenuProps } from "../ContextMenus/TubularsContextMenu";
import WbGeometryObjectContextMenu from "../ContextMenus/WbGeometryContextMenu";
import WellboreContextMenu, { WellboreContextMenuProps } from "../ContextMenus/WellboreContextMenu";
+import TrajectoriesContextMenu, { TrajectoriesContextMenuProps } from "../ContextMenus/TrajectoriesContextMenu";
import { IndexCurve } from "../Modals/LogPropertiesModal";
import LogTypeItem from "./LogTypeItem";
import ObjectGroupItem from "./ObjectGroupItem";
@@ -82,6 +83,13 @@ const WellboreItem = (props: WellboreItemProps): React.ReactElement => {
dispatchOperation({ type: OperationType.DisplayContextMenu, payload: { component: , position } });
};
+ const onTrajectoryContextMenu = (event: React.MouseEvent, wellbore: Wellbore, setIsLoading?: (arg: boolean) => void) => {
+ preventContextMenuPropagation(event);
+ const contextMenuProps: TrajectoriesContextMenuProps = { dispatchOperation, wellbore, servers, setIsLoading };
+ const position = getContextMenuPosition(event);
+ dispatchOperation({ type: OperationType.DisplayContextMenu, payload: { component: , position } });
+ };
+
const getExpandableObjectCount = useCallback(async () => {
if (wellbore.objectCount == null) {
setIsFetchingCount(true);
@@ -136,7 +144,12 @@ const WellboreItem = (props: WellboreItemProps): React.ReactElement => {
onGroupContextMenu={(event, _, setIsLoading) => onRigsContextMenu(event, wellbore, setIsLoading)}
/>
-
+ onTrajectoryContextMenu(event, wellbore, setIsLoading)}
+ />
+/// Create new trajectory tests.
+///
+public class CreateTrajectoryWorkerTests
+{
+ private const string Uid = "newTrajectoryUid";
+ private const string Name = "newTrajectoryName";
+ private const string WellUid = "wellUid";
+ private const string WellName = "wellName";
+ private const string WellboreUid = "wellboreUid";
+
+ private readonly Mock _witsmlClient;
+ private readonly CreateTrajectoryWorker _worker;
+
+ public CreateTrajectoryWorkerTests()
+ {
+ Mock witsmlClientProvider = new();
+ _witsmlClient = new Mock();
+ witsmlClientProvider.Setup(provider => provider.GetClient()).Returns(_witsmlClient.Object);
+ ILoggerFactory loggerFactory = new LoggerFactory();
+ loggerFactory.AddSerilog(Log.Logger);
+ ILogger logger = loggerFactory.CreateLogger();
+ _worker = new CreateTrajectoryWorker(logger, witsmlClientProvider.Object);
+ }
+
+ [Fact]
+ public async Task CreateTrajectory_Execute_MissingUid_InvalidOperationException()
+ {
+ var job = CreateJobTemplate(uid: null);
+ InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal("Uid cannot be empty", exception.Message);
+ job = CreateJobTemplate(uid: "");
+ exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal("Uid cannot be empty", exception.Message);
+ _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never);
+ }
+
+ [Fact]
+ public async Task CreateTrajectory_Execute_MissingName_InvalidOperationException()
+ {
+ var job = CreateJobTemplate(name: null);
+ InvalidOperationException exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal("Name cannot be empty", exception.Message);
+ job = CreateJobTemplate(name: "");
+ exception = await Assert.ThrowsAsync(() => _worker.Execute(job));
+ Assert.Equal("Name cannot be empty", exception.Message);
+ _witsmlClient.Verify(client => client.AddToStoreAsync(It.IsAny()), Times.Never);
+ }
+
+
+ [Fact]
+ public async Task CreateTrajectory_Execute_ValidResults()
+ {
+ CreateTrajectoryJob job = CreateJobTemplate();
+ List createdTrajectories = new();
+
+ _witsmlClient.Setup(client =>
+ client.AddToStoreAsync(It.IsAny()))
+ .Callback(trajectory => createdTrajectories.Add(trajectory))
+ .ReturnsAsync(new QueryResult(true));
+
+ await _worker.Execute(job);
+
+ Assert.Single(createdTrajectories);
+ Assert.Single(createdTrajectories.First().Trajectories);
+ WitsmlTrajectory createdObject = createdTrajectories.First().Trajectories.First();
+ Assert.Equal(Uid, createdObject.Uid);
+ Assert.Equal(Name, createdObject.Name);
+ Assert.Equal(WellUid, createdObject.UidWell);
+ Assert.Equal(WellName, createdObject.NameWell);
+ Assert.Equal(WellboreUid, createdObject.UidWellbore);
+ }
+
+ private static CreateTrajectoryJob CreateJobTemplate(string uid = Uid, string name = Name, string wellUid = WellUid, string wellName = WellName, string wellboreUid = WellboreUid)
+ {
+ return new CreateTrajectoryJob
+ {
+ Trajectory = new Trajectory()
+ {
+ Uid = uid,
+ Name = name,
+ WellUid = wellUid,
+ WellName = wellName,
+ WellboreUid = wellboreUid
+ }
+ };
+ }
+}