Skip to content

Commit

Permalink
2D Viewer module (#811)
Browse files Browse the repository at this point in the history
Co-authored-by: Hans Kallekleiv <[email protected]>
  • Loading branch information
rubenthoms and HansKallekleiv authored Dec 16, 2024
1 parent 3889e17 commit bd137c6
Show file tree
Hide file tree
Showing 97 changed files with 10,459 additions and 156 deletions.
2,001 changes: 1,848 additions & 153 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"test:ct:ui": "playwright test -c playwright.ct.config.ts --ui"
},
"dependencies": {
"@equinor/eds-core-react": "^0.42.5",
"@equinor/esv-intersection": "^3.0.10",
"@headlessui/react": "^1.7.8",
"@mui/base": "^5.0.0-beta.3",
Expand All @@ -28,9 +29,9 @@
"@tanstack/react-query-devtools": "^5.4.2",
"@types/geojson": "^7946.0.14",
"@webviz/group-tree-plot": "^1.1.14",
"@webviz/well-log-viewer": "^1.12.7",
"@webviz/subsurface-viewer": "^1.1.1",
"@webviz/well-completions-plot": "^1.5.11",
"@webviz/well-log-viewer": "^1.12.7",
"animate.css": "^4.1.1",
"axios": "^1.6.5",
"culori": "^3.2.0",
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/modules/2DViewer/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { InterfaceInitialization } from "@framework/UniDirectionalModuleComponentsInterface";

import { LayerManager } from "./layers/framework/LayerManager/LayerManager";
import { layerManagerAtom, preferredViewLayoutAtom } from "./settings/atoms/baseAtoms";
import { PreferredViewLayout } from "./types";

export type SettingsToViewInterface = {
layerManager: LayerManager | null;
preferredViewLayout: PreferredViewLayout;
};

export type Interfaces = {
settingsToView: SettingsToViewInterface;
};

export const settingsToViewInterfaceInitialization: InterfaceInitialization<SettingsToViewInterface> = {
layerManager: (get) => {
return get(layerManagerAtom);
},
preferredViewLayout: (get) => {
return get(preferredViewLayoutAtom);
},
};
83 changes: 83 additions & 0 deletions frontend/src/modules/2DViewer/layers/LayersActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from "react";

import { Menu } from "@lib/components/Menu";
import { MenuButton } from "@lib/components/MenuButton/menuButton";
import { MenuDivider } from "@lib/components/MenuDivider";
import { MenuHeading } from "@lib/components/MenuHeading";
import { MenuItem } from "@lib/components/MenuItem";
import { Dropdown } from "@mui/base";
import { Add, ArrowDropDown } from "@mui/icons-material";

export type LayersAction = {
identifier: string;
icon?: React.ReactNode;
label: string;
};

export type LayersActionGroup = {
icon?: React.ReactNode;
label: string;
children: (LayersAction | LayersActionGroup)[];
};

function isLayersActionGroup(action: LayersAction | LayersActionGroup): action is LayersActionGroup {
return (action as LayersActionGroup).children !== undefined;
}

export type LayersActionsProps = {
layersActionGroups: LayersActionGroup[];
onActionClick: (actionIdentifier: string) => void;
};

export function LayersActions(props: LayersActionsProps): React.ReactNode {
function makeContent(
layersActionGroups: (LayersActionGroup | LayersAction)[],
indentLevel: number = 0
): React.ReactNode[] {
const content: React.ReactNode[] = [];
for (const [index, item] of layersActionGroups.entries()) {
if (isLayersActionGroup(item)) {
if (index > 0) {
content.push(<MenuDivider key={index} />);
}
content.push(
<MenuHeading
key={`${item.label}-${index}`}
style={{ paddingLeft: `${indentLevel + 1}rem` }}
classNames="flex gap-2 items-center"
>
{item.icon}
{item.label}
</MenuHeading>
);
content.push(makeContent(item.children, indentLevel + 1));
} else {
content.push(
<MenuItem
key={`${item.identifier}-${index}`}
className="text-sm p-0.5 flex gap-2 items-center"
style={{ paddingLeft: `${indentLevel * 1}rem` }}
onClick={() => props.onActionClick(item.identifier)}
>
<span className="text-slate-700">{item.icon}</span>
{item.label}
</MenuItem>
);
}
}
return content;
}

return (
<Dropdown>
<MenuButton label="Add items">
<Add fontSize="inherit" />
<span>Add</span>
<ArrowDropDown fontSize="inherit" />
</MenuButton>
<Menu anchorOrigin="bottom-end" className="text-sm p-1 max-h-80 overflow-auto">
{makeContent(props.layersActionGroups)}
</Menu>
</Dropdown>
);
}
254 changes: 254 additions & 0 deletions frontend/src/modules/2DViewer/layers/delegates/GroupDelegate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { ItemDelegateTopic } from "./ItemDelegate";
import { PublishSubscribe, PublishSubscribeDelegate } from "./PublishSubscribeDelegate";
import { UnsubscribeHandlerDelegate } from "./UnsubscribeHandlerDelegate";

import { LayerManagerTopic } from "../framework/LayerManager/LayerManager";
import { SharedSetting } from "../framework/SharedSetting/SharedSetting";
import { DeserializationFactory } from "../framework/utils/DeserializationFactory";
import { Item, SerializedItem, instanceofGroup, instanceofLayer } from "../interfaces";

export enum GroupDelegateTopic {
CHILDREN = "CHILDREN",
TREE_REVISION_NUMBER = "TREE_REVISION_NUMBER",
CHILDREN_EXPANSION_STATES = "CHILDREN_EXPANSION_STATES",
}

export type GroupDelegateTopicPayloads = {
[GroupDelegateTopic.CHILDREN]: Item[];
[GroupDelegateTopic.TREE_REVISION_NUMBER]: number;
[GroupDelegateTopic.CHILDREN_EXPANSION_STATES]: { [id: string]: boolean };
};

/*
* The GroupDelegate class is responsible for managing the children of a group item.
* It provides methods for adding, removing, and moving children, as well as for serializing and deserializing children.
* The class also provides methods for finding children and descendants based on a predicate.
*/
export class GroupDelegate implements PublishSubscribe<GroupDelegateTopic, GroupDelegateTopicPayloads> {
private _owner: Item | null;
private _color: string | null = null;
private _children: Item[] = [];
private _publishSubscribeDelegate = new PublishSubscribeDelegate<GroupDelegateTopic>();
private _unsubscribeHandlerDelegate = new UnsubscribeHandlerDelegate();
private _treeRevisionNumber: number = 0;
private _deserializing = false;

constructor(owner: Item | null) {
this._owner = owner;
}

getColor(): string | null {
return this._color;
}

setColor(color: string | null) {
this._color = color;
}

prependChild(child: Item) {
this._children = [child, ...this._children];
this.takeOwnershipOfChild(child);
}

appendChild(child: Item) {
this._children = [...this._children, child];
this.takeOwnershipOfChild(child);
}

insertChild(child: Item, index: number) {
this._children = [...this._children.slice(0, index), child, ...this._children.slice(index)];
this.takeOwnershipOfChild(child);
}

removeChild(child: Item) {
this._children = this._children.filter((c) => c !== child);
this.disposeOwnershipOfChild(child);
this.incrementTreeRevisionNumber();
}

clearChildren() {
for (const child of this._children) {
this.disposeOwnershipOfChild(child);
}
this._children = [];
this.publishTopic(GroupDelegateTopic.CHILDREN);
this.incrementTreeRevisionNumber();
}

moveChild(child: Item, index: number) {
const currentIndex = this._children.indexOf(child);
if (currentIndex === -1) {
throw new Error("Child not found");
}

this._children = [...this._children.slice(0, currentIndex), ...this._children.slice(currentIndex + 1)];

this._children = [...this._children.slice(0, index), child, ...this._children.slice(index)];
this.publishTopic(GroupDelegateTopic.CHILDREN);
this.incrementTreeRevisionNumber();
}

getChildren() {
return this._children;
}

findChildren(predicate: (item: Item) => boolean): Item[] {
return this._children.filter(predicate);
}

findDescendantById(id: string): Item | undefined {
for (const child of this._children) {
if (child.getItemDelegate().getId() === id) {
return child;
}

if (instanceofGroup(child)) {
const descendant = child.getGroupDelegate().findDescendantById(id);
if (descendant) {
return descendant;
}
}
}

return undefined;
}

getAncestorAndSiblingItems(predicate: (item: Item) => boolean): Item[] {
const items: Item[] = [];
for (const child of this._children) {
if (predicate(child)) {
items.push(child);
}
}
const parentGroup = this._owner?.getItemDelegate().getParentGroup();
if (parentGroup) {
items.push(...parentGroup.getAncestorAndSiblingItems(predicate));
}

return items;
}

getDescendantItems(predicate: (item: Item) => boolean): Item[] {
const items: Item[] = [];
for (const child of this._children) {
if (predicate(child)) {
items.push(child);
}

if (instanceofGroup(child)) {
items.push(...child.getGroupDelegate().getDescendantItems(predicate));
}
}

return items;
}

makeSnapshotGetter<T extends GroupDelegateTopic>(topic: T): () => GroupDelegateTopicPayloads[T] {
const snapshotGetter = (): any => {
if (topic === GroupDelegateTopic.CHILDREN) {
return this._children;
}
if (topic === GroupDelegateTopic.TREE_REVISION_NUMBER) {
return this._treeRevisionNumber;
}
if (topic === GroupDelegateTopic.CHILDREN_EXPANSION_STATES) {
const expansionState: { [id: string]: boolean } = {};
for (const child of this._children) {
if (instanceofGroup(child)) {
expansionState[child.getItemDelegate().getId()] = child.getItemDelegate().isExpanded();
}
}
return expansionState;
}
};

return snapshotGetter;
}

getPublishSubscribeDelegate(): PublishSubscribeDelegate<GroupDelegateTopic> {
return this._publishSubscribeDelegate;
}

serializeChildren(): SerializedItem[] {
return this._children.map((child) => child.serializeState());
}

deserializeChildren(children: SerializedItem[]) {
if (!this._owner) {
throw new Error("Owner not set");
}

this._deserializing = true;
const factory = new DeserializationFactory(this._owner.getItemDelegate().getLayerManager());
for (const child of children) {
const item = factory.makeItem(child);
this.appendChild(item);
}
this._deserializing = false;
}

private incrementTreeRevisionNumber() {
this._treeRevisionNumber++;
this.publishTopic(GroupDelegateTopic.TREE_REVISION_NUMBER);
}

private takeOwnershipOfChild(child: Item) {
child.getItemDelegate().setParentGroup(this);

this._unsubscribeHandlerDelegate.unsubscribe(child.getItemDelegate().getId());

if (instanceofLayer(child)) {
this._unsubscribeHandlerDelegate.registerUnsubscribeFunction(
child.getItemDelegate().getId(),
child
.getItemDelegate()
.getPublishSubscribeDelegate()
.makeSubscriberFunction(ItemDelegateTopic.EXPANDED)(() => {
this.publishTopic(GroupDelegateTopic.CHILDREN_EXPANSION_STATES);
})
);
}

if (instanceofGroup(child)) {
this._unsubscribeHandlerDelegate.registerUnsubscribeFunction(
child.getItemDelegate().getId(),
child
.getGroupDelegate()
.getPublishSubscribeDelegate()
.makeSubscriberFunction(GroupDelegateTopic.TREE_REVISION_NUMBER)(() => {
this.incrementTreeRevisionNumber();
})
);
this._unsubscribeHandlerDelegate.registerUnsubscribeFunction(
child.getItemDelegate().getId(),
child
.getGroupDelegate()
.getPublishSubscribeDelegate()
.makeSubscriberFunction(GroupDelegateTopic.CHILDREN_EXPANSION_STATES)(() => {
this.publishTopic(GroupDelegateTopic.CHILDREN_EXPANSION_STATES);
})
);
}

this.publishTopic(GroupDelegateTopic.CHILDREN);
this.incrementTreeRevisionNumber();
}

private publishTopic(topic: GroupDelegateTopic) {
if (this._deserializing) {
return;
}
this._publishSubscribeDelegate.notifySubscribers(topic);
}

private disposeOwnershipOfChild(child: Item) {
this._unsubscribeHandlerDelegate.unsubscribe(child.getItemDelegate().getId());
child.getItemDelegate().setParentGroup(null);

if (child instanceof SharedSetting) {
this._owner?.getItemDelegate().getLayerManager().publishTopic(LayerManagerTopic.SETTINGS_CHANGED);
}

this.publishTopic(GroupDelegateTopic.CHILDREN);
}
}
Loading

0 comments on commit bd137c6

Please sign in to comment.