diff --git a/src/mol-state/immutable-tree.1.ts b/src/mol-state/immutable-tree.1.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee026fdb3e1a6d3577a35954242f555d78108d68 --- /dev/null +++ b/src/mol-state/immutable-tree.1.ts @@ -0,0 +1,280 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Map as ImmutableMap, OrderedSet } from 'immutable'; +import { Transform } from './transform'; + +export { ImmutableTree, TransientTree } + +/** + * An immutable tree where each node requires a unique reference. + * Represented as an immutable map. + */ +interface ImmutableTree { + readonly version: number, + readonly nodes: ImmutableTree.Nodes, + readonly root: ImmutableTree.Node, + get(ref: ImmutableTree.Ref): Transform | undefined, + //getChildren(ref: ImmutableTree.Ref): OrderedSet<ImmutableTree.Node> + asTransient(): TransientTree +} + +namespace ImmutableTree { + export type Ref = Transform.Ref + export interface Node extends Readonly<TransientTree.Node> { } + export interface Nodes extends ImmutableMap<ImmutableTree.Ref, Node> { } + + class Impl implements ImmutableTree { + readonly version: number; + readonly nodes: ImmutableTree.Nodes; + + get root() { return this.nodes.get(Transform.RootRef)! } + + get(ref: Ref) { + const n = this.nodes.get(ref); + return n ? n.value : void 0; + } + + asTransient(): TransientTree { + return new TransientTree(this); + } + + constructor(nodes: ImmutableTree.Nodes, version: number) { + this.nodes = nodes; + this.version = version; + } + } + + /** + * Create an instance of an immutable tree. + */ + export function create<T>(root: Transform): ImmutableTree { + const r: Node = { value: root, version: 0, parent: root.ref, children: OrderedSet() }; + return new Impl(ImmutableMap([[root.ref, r]]), 0); + } + + export function fromNodes<T>(nodes: Nodes, version: number): ImmutableTree { + return new Impl(nodes, version); + } + + type VisitorCtx = { nodes: Nodes, state: any, f: (node: Node, nodes: Nodes, state: any) => boolean | undefined | void }; + + function _postOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPostOrder(this, this.nodes.get(c!)!); } + function _doPostOrder(ctx: VisitorCtx, root: Node) { + if (root.children.size) { + root.children.forEach(_postOrderFunc, ctx); + } + ctx.f(root, ctx.nodes, ctx.state); + } + + /** + * Visit all nodes in a subtree in "post order", meaning leafs get visited first. + */ + export function doPostOrder<S>(tree: ImmutableTree, root: Node, state: S, f: (node: Node, nodes: Nodes, state: S) => boolean | undefined | void): S { + const ctx: VisitorCtx = { nodes: tree.nodes, state, f }; + _doPostOrder(ctx, root); + return ctx.state; + } + + function _preOrderFunc(this: VisitorCtx, c: ImmutableTree.Ref | undefined) { _doPreOrder(this, this.nodes.get(c!)!); } + function _doPreOrder(ctx: VisitorCtx, root: Node) { + const ret = ctx.f(root, ctx.nodes, ctx.state); + if (typeof ret === 'boolean' && !ret) return; + if (root.children.size) { + root.children.forEach(_preOrderFunc, ctx); + } + } + + /** + * Visit all nodes in a subtree in "pre order", meaning leafs get visited last. + * If the visitor function returns false, the visiting for that branch is interrupted. + */ + export function doPreOrder<S>(tree: ImmutableTree, root: Node, state: S, f: (node: Node, nodes: Nodes, state: S) => boolean | undefined | void): S { + const ctx: VisitorCtx = { nodes: tree.nodes, state, f }; + _doPreOrder(ctx, root); + return ctx.state; + } + + function _subtree(n: Node, nodes: Nodes, subtree: Node[]) { subtree.push(n); } + /** + * Get all nodes in a subtree, leafs come first. + */ + export function subtreePostOrder<T>(tree: ImmutableTree, root: Node) { + return doPostOrder<Node[]>(tree, root, [], _subtree); + } + + + function _visitChildToJson(this: Ref[], ref: Ref) { this.push(ref); } + interface ToJsonCtx { nodes: [Transform.Serialized, any, any[]][] } + function _visitNodeToJson(this: ToJsonCtx, node: Node) { + const children: Ref[] = []; + node.children.forEach(_visitChildToJson as any, children); + this.nodes.push([Transform.toJSON(node.value), node.parent, children]); + } + + export interface Serialized { + nodes: [any /** value */, number /** parent index */, number[] /** children indices */][] + } + + export function toJSON<T>(tree: ImmutableTree): Serialized { + const ctx: ToJsonCtx = { nodes: [] }; + + tree.nodes.forEach(_visitNodeToJson as any, ctx); + + const map = new Map<string, number>(); + let i = 0; + for (const n of ctx.nodes) map.set(n[0].ref, i++); + + for (const n of ctx.nodes) { + n[1] = map.get(n[1]); + const children = n[2]; + for (i = 0; i < children.length; i++) { + children[i] = map.get(children[i]); + } + } + return { + nodes: ctx.nodes + }; + } + + export function fromJSON<T>(data: Serialized): ImmutableTree { + const nodes = ImmutableMap<ImmutableTree.Ref, Node>().asMutable(); + + const values = data.nodes.map(n => Transform.fromJSON(n[0])); + let i = 0; + for (const value of values) { + const node = data.nodes[i++]; + const ref = value.ref; + nodes.set(ref, { + value, + version: 0, + parent: values[node[1]].ref, + children: OrderedSet(node[2].map(c => values[c].ref)) + }); + } + return new Impl(nodes.asImmutable(), 0); + } +} + +class TransientTree implements ImmutableTree { + nodes = this.tree.nodes.asMutable(); + version: number = this.tree.version + 1; + private mutations: Map<Transform.Ref, TransientTree.Node> = new Map(); + + get root() { return this.nodes.get(Transform.RootRef)! } + + get(ref: Transform.Ref) { + const n = this.nodes.get(ref); + return n ? n.value : void 0; + } + + asTransient() { + return this.asImmutable().asTransient(); + } + + mutate(ref: ImmutableTree.Ref): TransientTree.Node { + return mutateNode(this.nodes, this.mutations, ref); + } + + add(parentRef: ImmutableTree.Ref, value: Transform) { + const ref = value.ref; + ensureNotPresent(this.nodes, ref); + const parent = this.mutate(parentRef); + const node: TransientTree.Node = { version: 0, value, parent: parentRef, children: OrderedSet<string>().asMutable() }; + this.mutations.set(ref, node); + parent.children.add(ref); + this.nodes.set(ref, node); + return node; + } + + setValue(ref: ImmutableTree.Ref, value: Transform): TransientTree.Node { + checkSetRef(ref, value.ref); + const node = this.mutate(ref); + node.value = value; + return node; + } + + remove(ref: ImmutableTree.Ref): TransientTree.Node[] { + if (ref === Transform.RootRef) { + return this.removeChildren(ref); + } + + const { nodes, mutations } = this; + const node = nodes.get(ref); + if (!node) return []; + this.mutate(node.parent).children.delete(ref); + + const st = ImmutableTree.subtreePostOrder(this, node); + for (const n of st) { + nodes.delete(n.value.ref); + mutations.delete(n.value.ref); + } + + return st; + } + + removeChildren(ref: ImmutableTree.Ref): TransientTree.Node[] { + const { nodes, mutations } = this; + let node = nodes.get(ref); + if (!node || !node.children.size) return []; + node = this.mutate(ref); + const st = ImmutableTree.subtreePostOrder(this, node); + // remove the last node which is the parent + st.pop(); + node.children.clear(); + for (const n of st) { + nodes.delete(n.value.ref); + mutations.delete(n.value.ref); + } + return st; + } + + asImmutable() { + if (this.mutations.size === 0) return this.tree; + + this.mutations.forEach(m => (m as TransientTree.Node).children = m.children.asImmutable()); + return ImmutableTree.fromNodes(this.nodes.asMutable(), this.version); + } + + constructor(private tree: ImmutableTree) { + + } +} + +namespace TransientTree { + export interface Node { value: Transform, version: number, parent: ImmutableTree.Ref, children: OrderedSet<ImmutableTree.Ref> } +} + + +function checkSetRef(oldRef: ImmutableTree.Ref, newRef: ImmutableTree.Ref) { + if (oldRef !== newRef) { + throw new Error(`Cannot setValue of node '${oldRef}' because the new value has a different ref '${newRef}'.`); + } +} + +function ensureNotPresent(nodes: ImmutableTree.Nodes, ref: ImmutableTree.Ref) { + if (nodes.has(ref)) { + throw new Error(`Cannot add node '${ref}' because a different node with this ref already present in the tree.`); + } +} + +function ensurePresent(nodes: ImmutableTree.Nodes, ref: ImmutableTree.Ref) { + if (!nodes.has(ref)) { + throw new Error(`Node '${ref}' is not present in the tree.`); + } +} + +function mutateNode(nodes: ImmutableTree.Nodes, mutations: Map<ImmutableTree.Ref, TransientTree.Node>, ref: ImmutableTree.Ref): TransientTree.Node { + ensurePresent(nodes, ref); + if (mutations.has(ref)) { + return mutations.get(ref)!; + } + const node = nodes.get(ref)!; + const newNode: TransientTree.Node = { value: node.value, version: node.version + 1, parent: node.parent, children: node.children.asMutable() }; + mutations.set(ref, newNode); + nodes.set(ref, newNode); + return newNode; +} \ No newline at end of file diff --git a/src/mol-state/selection.ts b/src/mol-state/selection.ts index e61dd2611e541476f57e2c7419684974f49bdb3a..908e6b0dba5595b68e47c8d3ee30512684febaee 100644 --- a/src/mol-state/selection.ts +++ b/src/mol-state/selection.ts @@ -7,6 +7,7 @@ import { StateObject, StateObjectCell } from './object'; import { State } from './state'; import { ImmutableTree } from './immutable-tree'; +import { Transform } from './transform'; namespace StateSelection { export type Selector = Query | Builder | string | StateObjectCell; @@ -73,7 +74,7 @@ namespace StateSelection { export function root() { return build(() => (state: State) => [state.cells.get(state.tree.rootRef)!]) } - export function byRef(...refs: string[]) { + export function byRef(...refs: Transform.Ref[]) { return build(() => (state: State) => { const ret: StateObjectCell[] = []; for (const ref of refs) { diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts index f80137241fc821a9cf9dc49e3759da3a0ae2040d..99d15b81d4b116a670ddae11e03e5611c29e2467 100644 --- a/src/mol-state/transform.ts +++ b/src/mol-state/transform.ts @@ -17,7 +17,9 @@ export interface Transform<A extends StateObject = StateObject, B extends StateO } export namespace Transform { - export type Ref = string + export type Ref = string /* & { '@type': 'transform-ref' } */ + + export const RootRef = '-=root=-' as Ref; export interface Options { ref?: Ref, defaultProps?: unknown } @@ -70,7 +72,7 @@ export namespace Transform { return { transformer, params: pFromJson(t.params), - ref: t.ref, + ref: t.ref as Ref, version: t.version, defaultProps: t.defaultProps };