Source: graph/edge.js


import find from 'lodash/fp/find'
import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import flatten from 'lodash/fp/flatten'
import groupBy from 'lodash/fp/groupBy'
import toPairs from 'lodash/fp/toPairs'
import * as Port from '../port'
import * as Node from '../node'
import * as Edge from '../edge'
import {assertGraph} from '../assert'
import {equal, isRoot} from '../compoundPath'
import {node, port, hasPort, hasNode, nodesDeep, parent, replaceNode} from './node'
import * as changeSet from '../changeSet'
import {location, identifies as locIdentifies} from '../location'
import {incidents} from './connections'
import {access, store} from './internal'

/**
 * Returns a list of edges in the graph. Each edge also has an extra field identifying the parent
 * node to which the edge belongs in the hierarchy.
 * @example <caption>Getting all the edges in a graph</caption>
 * edgesDeep(graph) // -> [{from: ..., to: ..., layer: ..., parent: '#...'}, ...]
 * @param {PortGraph} graph The graph.
 * @returns {Edges[]} A list of edges.
 */
export function edgesDeep (graph) {
  return access('edgesDeep', graph) ||
        store(flatten(nodesDeep(graph).map((parent) =>
          edges(parent).map((e) => merge(e, {parent: Node.id(parent)})))), 'edgesDeep', graph)
}

/**
 * Returns a list of edges in the graph layer. Each edge also has an extra field identifying the parent
 * node to which the edge belongs in the hierarchy. This function will not recurse into compounds, use edgesDeep for that.
 * @example <caption>Getting the edges in a graph</caption>
 * edges(graph) // -> [{from: ..., to: ..., layer: ..., parent: '#...'}, ...]
 * @param {PortGraph} graph The graph.
 * @returns {Edges[]} A list of edges.
 */
export function edges (graph) {
  return access('edges', graph) || store((graph.edges || [])
    .map((edge) =>
      (edge.layer === 'dataflow' && hasPort(edge.from, graph))
        ? Edge.setType(Port.type(port(edge.from, graph)), edge)
        : edge), 'edges', graph)
}

export const checkEdge = curry((graph, edge) => {
  var from = node(edge.from, graph)
  var to = node(edge.to, graph)
  // TODO: check for edge/ from parent node is not correct anymore.. normEdge.from is a port object. (Same holds for normEdge.to)
  if (edge.from.node !== '' && !hasNode(edge.from, graph)) {
    throw new Error('Cannot create edge connection from not existing node: ' + Port.toString(edge.from) + ' to: ' + Port.toString(edge.to))
  } else if (edge.to.node !== '' && !hasNode(edge.to, graph)) {
    throw new Error('Cannot create edge connection from: ' + Port.toString(edge.from) + ' to not existing node: ' + Port.toString(edge.to))
  }
  if (edge.layer === 'dataflow') {
    if (typeof (edge.from) === 'string' || typeof (edge.to) === 'string') {
      throw new Error('A normalized edge is expected. No short-fort edges are allowed: ' + JSON.stringify(edge))
    } else if (Port.equal(edge.from, edge.to)) {
      throw new Error('Cannot add loops to the port graph from=to=' + Port.toString(edge.from))
    } else if (!Node.isReference(from) && !Node.hasPort(edge.from, from)) {
      throw new Error('The source node "' + Port.node(edge.from) + '" does not have the outgoing port "' + Port.portName(edge.from) + '".')
    } else if (!Node.isReference(to) && !Node.hasPort(edge.to, to)) {
      throw new Error('The target node "' + Port.node(edge.to) + '" does not have the ingoing port "' + Port.portName(edge.to) + '".')
    } else if (!Node.isReference(from) && (Node.port(edge.from, from).kind !== ((edge.innerCompoundOutput) ? 'input' : 'output'))) {
      throw new Error('The source port "' + Port.portName(edge.from) + '" = "' + JSON.stringify(Node.port(edge.from, from)) + '" must be ' +
      ((edge.innerCompoundOutput)
      ? 'an inner input port of the compound node ' + edgeParent(edge, graph)
      : 'an input port') + ' for the edge: ' + JSON.stringify(edge))
    } else if (!Node.isReference(to) && (Node.port(edge.to, to).kind !== ((edge.innerCompoundInput) ? 'output' : 'input'))) {
      throw new Error('The target port "' + Port.portName(edge.to) + '" = "' + JSON.stringify(Node.port(edge.to, to)) + ' must be ' +
        ((edge.innerCompoundInput)
        ? 'an inner output port of the compound node ' + edge.parent
        : 'an input port') + ' for the edge: ' + JSON.stringify(edge))
    }
  } else if (!edge.layer) {
    throw new Error('Edge must have a layer attribute: ' + JSON.stringify(edge))
  }
})

function pathsToIDs (edge, graph) {
  if (edge.query) return edge
  var from = node(edge.from, graph)
  var to = node(edge.to, graph)
  if (edge.layer === 'dataflow') {
    return merge(edge, {
      from: {node: Node.id(from)},
      to: {node: Node.id(to)}
    })
  } else {
    return merge(edge, {from: Node.id(from), to: Node.id(to)})
  }
}

function edgeParent (edge, graph) {
  var parentFrom = Node.path(parent(edge.from, graph))
  var parentTo = Node.path(parent(edge.to, graph))
  if (equal(parentFrom, parentTo)) {
    return parentFrom // = parentTo
  } else if (equal(Node.path(node(edge.from, graph)), parentTo)) {
    return parentTo
  } else if (equal(Node.path(node(edge.to, graph)), parentFrom)) {
    return parentFrom
  } else {
    if (edge.layer !== 'dataflow') {
      // TODO is there any sensible parent for an arbitrary edge?
      return parentFrom
    }
    throw new Error('Unable to determine parent for the edge:' + JSON.stringify(edge))
  }
}

function setInnerCompound (edge, graph) {
  if (edge.layer !== 'dataflow') return edge
  var parentFrom = Node.path(parent(edge.from, graph))
  var parentTo = Node.path(parent(edge.to, graph))
  var from = Node.path(node(edge.from, graph))
  var to = Node.path(node(edge.to, graph))
  if (equal(from, to) && equal(parentFrom, parentTo)) {
    return merge(edge, {innerCompoundInput: true, innerCompoundOutput: true})
  } else if (equal(from, parentTo)) {
    return merge(edge, {innerCompoundOutput: true})
  } else if (equal(to, parentFrom)) {
    return merge(edge, {innerCompoundInput: true})
  } else if (equal(parentFrom, parentTo)) {
    return edge
  } else {
    throw new Error('Unable to determine parent for the edge:', JSON.stringify(edge))
  }
}

function unIDPort (port, inner, graph) {
  var fromNode = node(port, graph)
  if (!Number.isNaN(parseInt(Port.portName(port)))) {
    if (Node.ports(fromNode).length === 0 && Node.isReference(fromNode)) return port
    var portId = parseInt(Port.portName(port))
    var ports = Node[inner ? 'inputPorts' : 'outputPorts'](fromNode, true)
    if (portId >= ports.length) {
      throw new Error('Node does not have a ' + portId + '-th port.')
    }
    return merge(port, {port: Port.portName(ports[portId])})
  }
  return port
}

function unIDPorts (edge, graph) {
  return merge(edge, {
    from: unIDPort(edge.from, edge.innerCompoundOutput, graph),
    to: unIDPort(edge.to, !edge.innerCompoundInput, graph)})
}

function normalize (edge, graph) {
  var normEdge = Edge.normalize(edge)
  return unIDPorts(setInnerCompound(pathsToIDs(normEdge, graph), graph), graph)
}

function addEdgeToCompound (edge, graph) {
  var cs = changeSet.insertEdge(edge)
  var parent = edgeParent(edge, graph)
  if (isRoot(parent) || equal(parent, graph.path)) {
    return changeSet.applyChangeSet(graph, cs)
  } else {
    var comp = node(parent, graph)
    var newComp = changeSet.applyChangeSet(comp, cs)
    return replaceNode(parent, newComp, graph)
  }
}

/**
 * @function
 * @name addEdge
 * @description Add an edge to the graph.
 * @param {Edge} edge The edge that should be added. This needn't be in standard format.
 * @param {PortGraph} graph The graph.
 * @returns {PortGraph} A new graph containing the edge.
 * @throws {Error} If:
 *  - the edge already exists
 *  - ports that the edge connects do not exists
 *  - nodes that the edge connects do not exists
 *  - the edge is not in normalizable form.
 */
export const addEdge = curry((edge, graph) => {
  assertGraph(graph, 2, 'addEdge')
  if (hasEdge(edge, graph)) {
    throw new Error('Cannot create already existing edge: ' + JSON.stringify(edge))
  }
  var normEdge = normalize(edge, graph)
  checkEdge(graph, normEdge)
  return addEdgeToCompound(normEdge, graph)
})

/**
 * @function
 * @name removeEdge
 * @description Remove an edge in the graph
 * @param {Edge} edge The edge that should be removed. This needn't be in standard format.
 * @param {PortGraph} graph The graph
 * @returns {PortGraph} A new graph that does not contain the edge anymore.
 * @throws {Error} If there is no such edge in the graph.
 */
export const removeEdge = curry((edge, graph) => {
  assertGraph(graph, 2, 'removeEdge')
  var normEdge = normalize(edge, graph)
  if (!hasEdge(normEdge, graph)) {
    throw new Error('Cannot delete edge that is not in the graph.')
  }
  var parent = edgeParent(edge, graph)
  const cs = changeSet.removeEdge(normEdge)
  if (isRoot(parent) || equal(parent, graph.path)) {
    return changeSet.applyChangeSet(graph, cs)
  } else {
    var comp = node(parent, graph)
    var newComp = changeSet.applyChangeSet(comp, cs)
    return replaceNode(parent, newComp, graph)
  }
})

function identifies (edge, graph) {
  assertGraph(graph, 2, 'identifies')
  if (!edge.query) return Edge.equal(edge)
  var fromLoc = location(edge.from, graph)
  var toLoc = location(edge.to, graph)
  return (cmpEdge) => locIdentifies(fromLoc, node(cmpEdge.from, graph)) &&
    (!fromLoc.port || fromLoc.port === cmpEdge.from.port) &&
    locIdentifies(toLoc, node(cmpEdge.to, graph)) &&
    (!toLoc.port || toLoc.port === cmpEdge.to.port)
}

function findEdge (edge, graph) {
  try { // TODO: improve error message when an error is thrown
    var normEdge = normalize(edge, graph)
    return find(identifies(normEdge, graph), edgesDeep(graph))
  } catch (err) {
    return null
  }
}

/**
 * @function
 * @name hasEdge
 * @description Checks whether the graph has the given edge.
 * @params {Edge} edge The edge to look for. This needn't be in standard format.
 * @params {PortGraph} graph The graph.
 * @returns {boolean} True if the edge is contained in the graph, false otherwise.
 */
export const hasEdge = curry((edge, graph) => {
  assertGraph(graph, 2, 'hasEdge')
  return !!findEdge(edge, graph)
})

/**
 * @function
 * @name edge
 * @description Returns the queried edge. This needn't be in standard format.
 * @params {Edge} edge A edge mock that only contains the connecting ports but not necessarily further information.
 * @params {PortGraph} graph The graph.
 * @returns {Edge} The edge as it is stored in the graph.
 * @throws {Error} If the edge is not contained in the graph.
 */
export const edge = curry((edge, graph) => {
  assertGraph(graph, 2, 'edge')
  var retEdge = findEdge(edge, graph)
  if (!retEdge) {
    throw new Error('Edge is not defined in the graph: ' + JSON.stringify(edge))
  }
  return retEdge
})

const realizePort = curry((node, type, port) => {
  if (Node.equal(Port.node(port), node)) {
    const pName = Port.portName(port)
    if (pName === parseInt(pName).toString()) {
      return Node[type + 'Port'](parseInt(pName), node)
    }
  }
})

function inputType (edge, port) {
  if (port === 'from' && !edge.innerCompoundOutput) return 'output'
  if (port === 'from' && edge.innerCompoundOutput) return 'input'
  if (port === 'to' && !edge.innerCompoundInput) return 'input'
  if (port === 'to' && edge.innerCompoundInput) return 'output'
}

function realizeEdge (edge, node) {
  if (edge.layer === 'dataflow') {
    return {
      from: realizePort(node, inputType(edge, 'from'), edge.from),
      to: realizePort(node, inputType(edge, 'to'), edge.to)
    }
  } else {
    return edge
  }
}

/**
 * @function
 * @name realizeEdgesForNode
 * @description This goes through all edges that are connected to the given node and
 * realizes them, if a node reference was replaced by an actual node. If it was replaced by another
 * reference nothing will happen.
 * @example <caption>Where it is used, when modifying graphs.</caption>
 * var graph = Graph.flow(
 *   Graph.addNode({ref: 'a', name: 'a', ports: []}),
 *   Graph.addNode({ref: 'b', name: 'b', ports: []}),
 *   Graph.addEdge({from: 'a@0', to: 'b@0'}),
 *   // the following command will call realizeEdgesForNode
 *   // it will replace the 0-th port in the edge with the 'outA' port.
 *   Graph.replaceNode('a', {componentId: 'a', ports: [{port: 'outA', kind: 'input', type: 'generic'}]})
 *   // the following command will call realizeEdgesForNode
 *   // it will replace the 0-th port in the edge with the 'outA' port.
 *   Graph.replaceNode('b', {componentId: 'b', ports: [{port: 'inB', kind: 'input', type: 'generic'}]})
 * )()
 * // after this the graph will have an edge {from: 'a@outA', to: 'b@inB'} and not {from: 'a@0', to: 'b@0'} anymore.
 * @param {Location} loc A location identifier for the node whose edges should be updated.
 * @param {Portgraph} graph The graph to perform the operation on
 * @returns {Portgraph} A new graph in which the edges for the given node are realized (if possible).
 */
export const realizeEdgesForNode = curry((loc, graph) => {
  assertGraph(graph, 2, 'realizeEdgesForNode')
  const nodeElem = node(loc, graph)
  if (Node.isReference(nodeElem)) return graph
  const edges = incidents(loc, graph)
  const cs = edges.map((e) => [e.parent, changeSet.updateEdge(e, realizeEdge(e, nodeElem))])
  return toPairs(groupBy((a) => a[0], cs))
    .map((v) => [v[0], v[1].map((i) => i[1])])
    .reduce((gr, c) => replaceNode(c[0], changeSet.applyChangeSets(node(c[0], gr), c[1]), gr), graph)
})