/**
* @module Graph.internal
* @overview
* This methods are for internal usage. They do not check for bad inputs and can create broken graphs.
* If you know what you are doing you can include them via `import * as GraphInternals from '@buggyorg/graphtools/graph/internal'`.
*/
import curry from 'lodash/fp/curry'
import flatten from 'lodash/fp/flatten'
import pick from 'lodash/fp/pick'
import set from 'lodash/fp/set'
import setIn from 'lodash/set'
import omit from 'lodash/fp/omit'
import merge from 'lodash/fp/merge'
import * as Node from '../node'
import {equal as pathEqual, isRoot, relativeTo, join} from '../compoundPath'
import {setPath as compoundSetPath} from '../compound'
import * as changeSet from '../changeSet'
import {flowCallback} from './flow'
/**
* @function
* @name nodes
* @description Returns a list of nodes on the root level.
* @param {PortGraph} graph The graph.
* @param {function} [predicate] An optional function that filters nodes. If no predicate function is given, all nodes are returned.
* @returns {Nodes[]} A list of nodes.
*/
export const nodes = (graph) => {
return graph.nodes || []
}
function nodesDeepRec (graph, parents) {
return flatten(parents.map(nodesDeepInternal))
}
function nodesDeepInternal (graph) {
return nodes(graph)
.concat(nodesDeepRec(graph, nodes(graph)))
}
export function store (what, key, graph) {
if (!graph.___store) initStore(graph)
setIn(graph.___store, key, what)
return what
}
export function access (key, graph) {
return (graph.___store) ? graph.___store[key] : undefined
}
export function forget (key, graph) {
if (graph.___store) {
delete graph.___store[key]
}
return graph
}
function initStore (graph) {
Object.defineProperty(graph, '___store', {configurable: true, enumerable: false, value: {}})
}
export function resetStore (graph) {
delete graph.___store
initStore(graph)
}
/**
* Get all nodes at all depths. It will go into every compound node and return their nodes
* and the nodes of their compound nodes, etc.
* @param {PortGraph} graph The graph to work on
* @returns {Node[]} A list of nodes.
*/
export function nodesDeep (graph) {
return access('nodesDeep', graph) || store(nodesDeepInternal(graph).concat([graph]), 'nodesDeep', graph)
}
/**
* Returns a node that is located at a specific path in the graph.
* @param {CompoundPath} path The path to the wanted node.
* @param {PortGraph} graph The graph
* @returns {Node|undefined} The node or undefined if the path does not exist.
*/
export function nodeByPath (path, graph) {
if (!path) return
if (isRoot(path)) return graph
return nodeBy((n) => pathEqual(path, n.path), graph)
// return nodeByPathRec(graph, path, path)
}
/**
* Find a node using a predicate.
* @param {Function} fn A function that decides for each node if it should be rejected or not
* @param {PortGraph} graph The graph
* @returns {Node|undefined} The first node that matches the predicate.
*/
export function nodeBy (fn, graph) {
return nodesDeep(graph).filter(fn)[0]
}
/**
* @function
* @name idToPath
* @description Returns the path that points to the node in the graph by its id. The id is preserved when moving or editing nodes.
* The path might change. To fixate a node one can use the ID.
* @param {string} id The id of the node
* @param {PortGraph} graph The graph to search in
* @returns {CompoundPath|null} The path to the node with the given ID.
*/
export const idToPath = curry((id, graph) => {
return access('path-' + id, graph) || store(nodesDeep(graph).find((n) => n.id === id).path, 'path-' + id, graph)
// return nodesDeep(graph).find((n) => n.id === id).path
})
function replacePortIDs (port, id, replaceId) {
if (port.node === replaceId) return set('node', id, port)
else return port
}
export function replaceEdgeIDs (edges, id, replaceId) {
return edges.map((edge) => {
if (typeof (edge.to) === 'object') {
return set('to', replacePortIDs(edge.to, id, replaceId),
set('from', replacePortIDs(edge.from, id, replaceId), edge))
} else {
return set('to', (edge.to === replaceId) ? id : edge.to,
set('from', (edge.from === replaceId) ? id : edge.from, edge))
}
})
}
/**
* @function
* @name mergeNodes
* @description Merges the contents of a node with the given data. This CAN destroy the structure of the
* graph so be cautious and prefer updateNode whenever possible.
* @param {Node} oldNode The old node that should get updated
* @param {Object} newNode New values for the old node as an object that gets merged into the node.
* @param {PortGraph} graph The graph
* @param {Callback} [cb] A callback function that is called with the newly inserted node.
* @returns {PortGraph} The new graph with the merged nodes.
*/
export const mergeNodes = curry((oldNode, newNode, graph, ...cbs) => {
const cb = flowCallback(cbs)
var path = idToPath(newNode.id, graph)
if (!graph.inplace) {
const mergeGraph = changeSet.applyChangeSet(graph,
changeSet.updateNode(relativeTo(path, graph.path), merge(
pick(['id', 'name', 'path'], oldNode), {edges: replaceEdgeIDs(newNode.edges || [], oldNode.id, newNode.id)})))
return cb(nodeByPath(path, graph), mergeGraph)
}
return cb(nodeByPath(path, graph), changeSet.applyChangeSet(graph, // parent(oldNode, graph),
changeSet.setNode(
relativeTo(path, graph.path),
Object.assign({},
newNode,
pick(['id', 'name', 'path'], oldNode),
{edges: (oldNode.id === newNode.id) ? newNode.edges : replaceEdgeIDs(newNode.edges || [], oldNode.id, newNode.id)})
)))
})
/**
* Updates all paths in the graph.
* @param {PortGraph} graph The graph to update
* @returns {PortGraph} The port graph with all valid paths.
*/
export const rePath = (graph) => {
graph.path = graph.path || []
return rePathRec(graph.path, graph)
}
const rePathRec = (basePath, graph) => {
nodes(graph).forEach((n) => {
var newPath = join(basePath, Node.id(n))
n.path = newPath
if (Node.hasChildren(n)) {
rePathRec(newPath, n)
}
})
return graph
}
function setPath (node, path) {
var nodePath = join(path, Node.id(node))
if (Node.hasChildren(node)) {
return compoundSetPath(node, nodePath, setPath)
}
return merge(node, {path: nodePath})
}
export const unID = (node) => {
return omit(['id', 'path'], node)
}
export function addNodeInternal (node, graph, path, checkNode, ...cbs) {
const cb = flowCallback(cbs)
var newNode
newNode = setPath(Node.create(unID(node)), path)
checkNode(graph, newNode)
if (Node.hasChildren(newNode)) {
newNode = set('edges', replaceEdgeIDs(newNode.edges, newNode.id, node.id), newNode)
}
return cb(newNode, changeSet.applyChangeSet(graph, changeSet.insertNode(newNode, path)))
}