Skip to content

Commit

Permalink
Commissioing package react components (#45)
Browse files Browse the repository at this point in the history
###
[AB#201347](https://dev.azure.com/EquinorASA/bb9bd8cb-74f7-4ffa-b0cb-60eff0a0be58/_workitems/edit/201347)

## Aim of the PR
- [x] Make it possible to select equipment, piping components as
boundary or internal component
- [ ] Make it possible to select piping lines as boundary or internal
component
- [x] Based on the boundary and the internal component the resulting
commissioning package is highlighted
- [ ] The piping lines is highlighted as well

#### Suggestion
I did not reach every goal for this PR. I decided to give up on
implementing the highlighting and selection of the piping lines as this
was too difficult. In order to achieve highlighting on the piping lines
we need to assign the correct rdf IRI on these lines. The logic for
which IRI a line should have is connected with what preceding and
following siblings the CenterLine element has in the XML. We do not have
access to this information anymore since the XML is parsed to react
components.

I suggest that we address this issue when creating the new RDF format
for the front-end. Every component on the new format needs to have the
RDF IRI attached to it. With this in place it should be no issue to also
add highlighting on the centerlines.

## Implementation 

### New components

#### CommissioningpackageContext
- Added internalIds, boundaryIds as well as state setters to update
them.

#### handleAddInternal and handleAddBoundary
When a component is selected as internal or boundary then these
functions are triggered.
- They will add the id of the selected component to borderIds or
boundaryIds on the commissioning context.
- Makes sure that a component cant be selected as internal or boundary
at the same time
- Updates rdfox

#### ClickableComponentProps
- Properties for dealing with clickable components, as well as two
functions that is used by clickable components for determining highlight
color and what actions to take when a component is clicked.

#### StyledSvgElement
- Creates a svg element with highlighting. 

### Logic

The functions for creating the PipingComponent and Equipment react
components now accepts ClickableComponentProps as argument. The
ClickableComponentProps holds information on which function should be
triggered on click, and on shift click. The functions handleAddInternal
and handleAddBoundary is used as arguments here. The <StyledSvgElement>
will also be created if the equipment or piping component is internal,
boundary or in the commissioning package. This creates the highlighting.

When the context for the commissioning package changes a call is made to
rdfox to check if there are any nodes in the commissioning package, if
this is true then the context is updated with these new ids. When the
context is updated then they will be highlighted in yellow.

Upon refresh of the page rdfox is wiped, and the commissioning package
context is reset.

## Type of change
- [ ] Bug fix 
- [x] New feature 
- [ ] Breaking change 
- [ ] This change requires a documentation update

## How Has This Been Tested?
Tested the solution manually by selecting equipment and piping
components as both boundary and internal. Checked that the highlighting
appeared on the correct elements. Also checked that rdfox and the
front-end was in sync by querying rdfox in between selecting boundaries
and internals.

## Additional Changes
- Previously the SvgElement function required a height as argument. The
height parameter has been removed from this function. I therefore
removed this argument from where this function was called.
- Some refactoring in Triplestore.ts -> moved logic for querying and
updating the Triplestore to one function. This function has a method
param, when the method is GET it queries rdfox, when the method is POST
it will delete or insert new data.
- Deleted file named CommissioningPackage.ts -> this information was
already present in CommissioningPackageContext.
  • Loading branch information
henriettelienrebnor authored Dec 2, 2024
1 parent e27d963 commit 16628b4
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 135 deletions.
1 change: 0 additions & 1 deletion www/src/components/ActuatingSystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default function ActuatingSystem(props: ActuatingSystemProps) {
{actuatingSystemComponents.map((component, index: number) => (
<SvgElement
key={index}
height={height}
componentName={component.ComponentName!}
id={component.ID}
position={component.Position}
Expand Down
49 changes: 18 additions & 31 deletions www/src/components/Equipment.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,45 @@
import { EquipmentProps, NozzleProps } from "../types/diagram/Diagram.ts";
import { useContext } from "react";
import styled from "styled-components";
import { ClickableComponentProps, handleClick, getHighlightColor } from "../types/ClickableComponentProps.ts";
import StyledSvgElement from "./StyledSvgElement.tsx";
import PandidContext from "../context/PandidContext.ts";
import useSerializeNodeSvg from "../hooks/useSerializeNodeSvg.tsx";
import { BoundaryActions, BoundaryParts } from "../utils/Triplestore.ts";
import SvgElement from "./SvgElement.tsx";
import { useCommissioningPackageContext } from "../hooks/useCommissioningPackageContext.tsx";

const StyledG = styled.g`
path {
stroke: yellow;
stroke-width: 5;
}
`;

interface EquipmentComponentProps {

interface EquipmentClickableProps {
equipment: EquipmentProps;
onClick: (
id: string,
action: BoundaryActions,
type: BoundaryParts,
) => Promise<void>;
isBoundary: boolean;
clickableComponent: ClickableComponentProps
}

export default function Equipment({
equipment,
onClick,
isBoundary,
}: EquipmentComponentProps) {
clickableComponent
}: EquipmentClickableProps) {
const packageContext = useCommissioningPackageContext()
const height = useContext(PandidContext).height;
const svg = useSerializeNodeSvg(
equipment.ComponentName,
equipment.GenericAttributes[0],
);
const color = getHighlightColor(equipment.ID, packageContext);

const nozzles: NozzleProps[] = equipment.Nozzle;

return (
<g
onClick={() =>
onClick(equipment.ID, BoundaryActions.Insert, BoundaryParts.Boundary)
}
onClick={handleClick(clickableComponent, packageContext, equipment.ID)}
>
{svg && (
{svg && color && (
<>
{isBoundary && (
<StyledG
id={equipment.ID + "_highlight"}
transform={`${equipment.Position.Reference.X === -1 ? "rotate(-180deg)" : ""}translate(${equipment.Position.Location.X}, ${height - equipment.Position.Location.Y})`}
className={".node"}
dangerouslySetInnerHTML={{ __html: svg }}
/>
)}
<StyledSvgElement
id={equipment.ID + "_highlight"}
position={equipment.Position}
svg={svg}
color={color}
/>
<g
id={equipment.ID}
transform={`${equipment.Position.Reference.X === -1 ? "rotate(-180deg)" : ""}translate(${equipment.Position.Location.X}, ${height - equipment.Position.Location.Y})`}
Expand All @@ -66,7 +54,6 @@ export default function Equipment({
key={index}
id={nozzle.ID}
componentName={nozzle.ComponentName || "ND0002_SHAPE"}
height={height}
position={nozzle.Position}
text={nozzle.GenericAttributes}
/>
Expand Down
122 changes: 109 additions & 13 deletions www/src/components/Pandid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import Equipment from "./Equipment.tsx";
import { useCallback, useEffect, useState } from "react";
import ProcessInstrumentationFunction from "./ProcessInstrumentationFunction.tsx";
import { EquipmentProps, XMLProps } from "../types/diagram/Diagram.ts";
import { PipingNetworkSystemProps } from "../types/diagram/Piping.ts";
import { PipingComponentProps, PipingNetworkSegmentProps, PipingNetworkSystemProps } from "../types/diagram/Piping.ts";
import { ProcessInstrumentationFunctionProps } from "../types/diagram/ProcessInstrumentationFunction.ts";
import { ensureArray } from "../utils/HelperFunctions.ts";
import { ActuatingSystemProps } from "../types/diagram/ActuatingSystem.ts";
import ActuatingSystem from "./ActuatingSystem.tsx";
import PandidContext from "../context/PandidContext.ts";
import PipeSystem from "./piping/PipeSystem.tsx";
import PipeSegment from "./piping/PipeSegment.tsx";
import React from "react";
import {
BoundaryActions,
BoundaryParts,
makeSparqlAndUpdateStore,
getNodeIdsInCommissioningPackage,
cleanTripleStore,
} from "../utils/Triplestore.ts";
import { useCommissioningPackageContext } from "../hooks/useCommissioningPackageContext.tsx";
import PipingComponent from "./piping/PipingComponent.tsx";
import { CommissioningPackageProps } from "../context/CommissioningPackageContext.tsx";

export default function Pandid() {
const [xmlData, setXmlData] = useState<XMLProps | null>(null);
Expand All @@ -33,6 +40,7 @@ export default function Pandid() {
attributeNamePrefix: "",
});


// Read XML file from disk, parse as XMLProps (TypeScript interface)
useEffect(() => {
fetch("/DISC_EXAMPLE-02-02.xml")
Expand All @@ -43,6 +51,34 @@ export default function Pandid() {
});
}, []);

//Clean triplestore on render
useEffect(() => {
(async () => {
await cleanTripleStore();
context.setCommissioningPackages([]);
context.setboundaryIds([]);
context.setInternalIds([]);
})()
}, [])

useEffect(() => {
(async () => {
const nodeIds = await getNodeIdsInCommissioningPackage();
//TODO: This logic needs to be improved when introducing multiple commissioning packages.
// Default package name "asset:Package1" used.
if (context.commissioningPackages.length < 1) {
const newPackage: CommissioningPackageProps = {
id: "asset:Package1",
idsInPackage: nodeIds
}
context.setCommissioningPackages([newPackage]);
context.setActivePackageId(newPackage.id);
} else {
context.setCommissioningPackages(getUpdatedCommissioningPackages(nodeIds))
}
})();
}, [context]);

// When XML data is loaded, set all component states
useEffect(() => {
if (!xmlData) return;
Expand All @@ -54,17 +90,56 @@ export default function Pandid() {
setActuatingSystem(xmlData.PlantModel.ActuatingSystem);
}, [xmlData]);

const handleAddInternal = useCallback(
async (id: string, action: BoundaryActions) => {
context.setInternalIds((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]);
await makeSparqlAndUpdateStore(id, action, BoundaryParts.InsideBoundary);

if (context.boundaryIds.includes(id)) {
context.setboundaryIds(prev => prev.filter((item) => item !== id));
await makeSparqlAndUpdateStore(id, BoundaryActions.Delete, BoundaryParts.Boundary);
}
},
[context],
);

const handleAddBoundary = useCallback(
async (id: string, action: BoundaryActions, type: BoundaryParts) => {
context.setBorderIds((prev) =>
async (id: string, action: BoundaryActions) => {
context.setboundaryIds((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
);
await makeSparqlAndUpdateStore(id, action, type);
},

[],
if(context.internalIds.includes(id)) {
context.setInternalIds(prev => prev.filter((item) => item !== id));
await makeSparqlAndUpdateStore(id, BoundaryActions.Delete, BoundaryParts.InsideBoundary);
}

await makeSparqlAndUpdateStore(id, action, BoundaryParts.Boundary);
},
[context],
);

useEffect(() => {
console.log(`internals: ${context.internalIds}`);
console.log(`boundaries: ${context.boundaryIds}`);
}, [context.boundaryIds, context.internalIds])


const getUpdatedCommissioningPackages = (ids: string[]) => {
return context.commissioningPackages.map(pkg => {
if (pkg.id === context.activePackageId) {
const updatedPackage: CommissioningPackageProps = {
id: "asset:Package1",
idsInPackage: ids
};
return updatedPackage;
} else {
return pkg;
};
})
};

return (
<>
{xmlData && (
Expand All @@ -80,17 +155,38 @@ export default function Pandid() {
equipments.map((equipment: EquipmentProps, index: number) => (
<Equipment
key={index}
isBoundary={context.borderIds.includes(equipment.ID)}
equipment={equipment}
onClick={handleAddBoundary}
clickableComponent={{
onClick: handleAddBoundary,
onShiftClick: handleAddInternal,
}}
/>
))}
{pipingNetworkSystems &&
pipingNetworkSystems.map(
(piping: PipingNetworkSystemProps, index: number) => (
<PipeSystem key={index} {...piping} />
),
)}
pipingNetworkSystems.map((pipingNetworkSystem: PipingNetworkSystemProps, index: number) => (
<React.Fragment key={index}>
<PipeSystem {...pipingNetworkSystem} />

{ensureArray(pipingNetworkSystem.PipingNetworkSegment).map((pipingNetworkSegment: PipingNetworkSegmentProps, segmentIndex: number) => (
<React.Fragment key={segmentIndex}>
<PipeSegment {...pipingNetworkSegment} />

{pipingNetworkSegment.PipingComponent &&
ensureArray(pipingNetworkSegment.PipingComponent).map((pipingComponent: PipingComponentProps, componentIndex: number) => (
<PipingComponent
key={componentIndex}
pipingComponent={pipingComponent}
clickableComponent={{
onClick: handleAddBoundary,
onShiftClick: handleAddInternal
}}
/>
))}
</React.Fragment>
))}
</React.Fragment>
))
}
{processInstrumentationFunction &&
processInstrumentationFunction.map(
(
Expand Down
54 changes: 54 additions & 0 deletions www/src/components/StyledSvgElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
PositionProps,
} from "../types/diagram/Common.ts";
import calculateAngleAndRotation from "../utils/Transformation.ts";
import { useContext } from "react";
import PandidContext from "../context/PandidContext.ts";
import styled from "styled-components"

interface StyledSvgElementProps {
id: string;
position?: PositionProps;
svg: string;
color: string;
}

const StyledG = styled.g`
path {
stroke: ${(props) => props.color};
stroke-width: 5;
opacity: 0.5 ;
}
`

export default function StyledSvgElement({
id,
position,
svg,
color
}: StyledSvgElementProps) {
const height = useContext(PandidContext).height;

return (
<>
{svg && (
<StyledG
id={id}
color={color}
transform={
position
? calculateAngleAndRotation(
position.Reference.X,
position.Reference.Y,
position.Location.X,
height - position.Location.Y,
)
: ""
}
className={".node"}
dangerouslySetInnerHTML={{ __html: svg }}
/>
)}
</>
);
}
18 changes: 1 addition & 17 deletions www/src/components/piping/PipeSegment.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import CenterLine from "../CenterLine.tsx";
import {
PipingComponentProps,
PipingNetworkSegmentProps,
} from "../../types/diagram/Piping.ts";
import { CenterLineProps } from "../../types/diagram/Common.ts";
import PipingComponent from "./PipingComponent.tsx";
import { useContext } from "react";
import PandidContext from "../../context/PandidContext.ts";
import SvgElement from "../SvgElement.tsx";
Expand All @@ -16,25 +14,13 @@ export default function PipeSegment(props: PipingNetworkSegmentProps) {
const centerlines: CenterLineProps[] = Array.isArray(props.CenterLine)
? props.CenterLine
: [props.CenterLine];
const pipingComponents: PipingComponentProps[] = Array.isArray(
props.PipingComponent,
)
? props.PipingComponent
: [props.PipingComponent];


return (
<>
<CenterLine centerLines={centerlines} isInformationFlow={false} />
{pipingComponents &&
pipingComponents[0] !== undefined &&
pipingComponents.map(
(pipingComponent: PipingComponentProps, index: number) => (
<PipingComponent key={index} {...pipingComponent} />
),
)}
{props.PipeSlopeSymbol && (
<SvgElement
height={height}
componentName={props.PipeSlopeSymbol.ComponentName}
id={props.PipeSlopeSymbol.ID}
position={props.PipeSlopeSymbol.Position}
Expand All @@ -45,7 +31,6 @@ export default function PipeSegment(props: PipingNetworkSegmentProps) {
<SvgElement
id={props.PropertyBreak.ID}
componentName={props.PropertyBreak.ComponentName}
height={height}
text={props.PropertyBreak.GenericAttributes[0]}
position={props.PropertyBreak.Position}
/>
Expand All @@ -59,7 +44,6 @@ export default function PipeSegment(props: PipingNetworkSegmentProps) {
)}
{props.PipeOffPageConnector && (
<SvgElement
height={height}
componentName={props.PipeOffPageConnector.ComponentName}
id={props.PipeOffPageConnector.ID}
position={props.PipeOffPageConnector.Position}
Expand Down
Loading

0 comments on commit 16628b4

Please sign in to comment.