/**
* A location is an object that defines a port or a node in the graph. A location can be one of the following:
* - id,
* - compound path
* - node object
* - port object.
*/
import curry from 'lodash/fp/curry'
import merge from 'lodash/fp/merge'
import {nodeByPath, idToPath, nodes} from './graph/internal'
import {isPort} from './port'
import {isValid as isNode, equal, id, isID, isReference} from './node'
import {rest, prefix} from './compoundPath'
/** A port notation can have every of the other notations for the node and as such
* it is necessary to check first if it is a port notation. The symbol '@' is only used in
* port notations.
*/
function isPortNotation (str) {
return str.indexOf('@') !== -1
}
function isCompoundPathNotation (str) {
return !isPortNotation(str) && str[0] === '»'
}
function isComponent (str) {
return !isPortNotation(str) && str[0] === '/'
}
function isIndex (str) {
return !isPortNotation(str) && str[0] === '#'
}
function isRoot (str) {
return str === ''
}
/*
function isName (str) {
return !isPortNotation(str) && !isIndex(str) && !isCompoundPathNotation(str)
}
*/
function parsePortNotation (port) {
var split = port.split('@')
if (split[1] === '') {
throw new Error('Invalid port notation. Port notation does not contain a port. Parsed port: ' + port)
}
return merge(fromString(split[0], false), {type: 'location', locType: 'port', port: split[1]})
}
/**
* Converts a compound path string into its array representation. The seperate parts must be divided by a '»'.
* @param {String} compoundPathStr A string reperesenting the compound path divded by '»'.
* @returns {String[]} An array of node IDs representing the compound path.
*/
export function parseCompoundPath (compoundPathStr) {
if (compoundPathStr.indexOf('»') === -1) return [compoundPathStr]
return compoundPathStr.split('»').slice(1)
}
/** Creates a location object from the string representation */
function fromString (str, allowsPorts = true) {
if (isPortNotation(str)) {
if (!allowsPorts) {
throw new Error('Found unexpected port notation. Do you have multiple @\'s in your location string?')
}
return parsePortNotation(str)
} else if (isCompoundPathNotation(str)) {
return {type: 'location', locType: 'node', path: parseCompoundPath(str)}
} else if (isComponent(str)) {
return {type: 'query', queryType: 'component', query: str.slice(1)}
} else if (isIndex(str)) {
return {type: 'location', locType: 'node', index: str}
} else if (isRoot(str)) {
return {type: 'location', locType: 'node', path: []}
} else {
return {type: 'location', locType: 'node', path: [str]}
}
}
function idify (path, graph) {
if (path.length === 0) return path
if (path.every((p) => isID(p))) return path
var node = nodes(graph).filter(equal(path[0]))[0]
if (!node) {
if (equal(path[0], graph)) {
return [id(graph)].concat(idify(rest(path), graph))
}
return
}
return [id(node)].concat(idify(rest(path), node))
}
function locPath (loc, graph) {
var graphPath = graph.path
if (loc.path) {
let idPath = idify(loc.path, graph)
return (idPath) ? prefix(idPath, graphPath) : undefined
} else if (loc.name) {
// best guess is that the node is at the root level...
let idPath = idify([loc.name], graph)
return (idPath) ? prefix(idPath, graphPath) : undefined
} else if (loc.index) {
var nodePath = idToPath(loc.index, graph)
if (!nodePath) {
throw new Error('Unable to locate node with id: ' + loc.index + '.')
}
return prefix(nodePath, graphPath)
} else {
throw new Error('Unable to process location. Not enough information to find node in the graph.')
}
}
function fullLocation (loc, graph) {
if (loc.type === 'query') return loc
var path = locPath(loc, graph)
var node = nodeByPath(path, graph) || {}
return {
type: 'location',
locType: loc.locType,
path,
index: (loc.index) ? loc.index : node.id,
name: (loc.name) ? loc.name : node.name,
port: loc.port
}
}
/**
* Create a new location from a given object
* @param loc Any processable form of location. TODO: list alle formats.
* @param {PortGraph} graph The graph in which the location is valid.
* @returns {Location} A location object
*/
export function location (loc, graph) {
if (typeof (loc) === 'string') {
return fullLocation(fromString(loc), graph)
} else if (Array.isArray(loc)) {
return fullLocation({type: 'location', locType: 'node', path: loc}, graph)
} else if (typeof (loc) === 'object' && isPort(loc)) {
var locObj = location(loc.node, graph)
var merged = (locObj.type === 'query')
? merge({locType: 'port', port: loc.port}, location(loc.node, graph))
: merge(location(loc.node, graph), {type: 'location', locType: 'port', port: loc.port})
return fullLocation(merged, graph)
} else if (typeof (loc) === 'object' && loc.id) {
return fullLocation(fromString(loc.id), graph)
} else if (typeof (loc) === 'object' && loc.name) {
return fullLocation(fromString(loc.name), graph)
} else if (typeof (loc) === 'object' && loc.path) {
return fullLocation({type: 'location', locType: 'node', path: loc.path}, graph)
} else {
return {type: 'location', locType: 'invalid'}
// throw new Error('Unknown location type: ' + JSON.stringify(loc))
}
}
/**
* Create a query function for a location.
* @param {Location} loc A location identifier.
* @param {PortGraph} graph The graph in which the location is valid.
* @returns {function} A function that takes another location or location identifier
* and compares it to the specified location `loc`. See `Location.identifies`.
*/
export function query (loc, graph) {
return identifies(location(loc, graph))
}
function identifiesNode (loc, node) {
return (isPort(node) &&
((isIndex(node.node) && loc.index === node.node) ||
(!isIndex(node.node) && loc.name === node.node))) ||
(!isPort(node) && loc.index === node.id)
}
function identifiesPort (loc, port) {
return loc.locType === 'port' && loc.port === port.port && identifiesNode(loc, port)
}
function isRootNode (n) {
return typeof (n) === 'object' && n.path && n.path.length === 0
}
/**
* @function
* @name identifies
* @description Checks whether a location identifies the given object. This is true if
* for example the location points to a node and the other object is the node.
* Or if the other object is simply the ID of the node.
* It also identifies a node if the location specifies the port. If you don't want
* this behavior use equals (it is the strict version of identifies).
* @params {Location} location A location object.
* @params other Any type that can be a location.
* @returns True if the location identifies the object stored in other.
*/
export const identifies = curry((loc, other) => {
if (loc.type === 'query' && loc.queryType === 'component') {
if (isReference(other)) {
return other.ref === loc.query
}
if (isPort(other)) {
return identifies(loc, other.additionalInfo)
}
return other.componentId === loc.query
} else if (loc.locType === 'port' && isPort(other)) {
return isPort(other) && identifiesPort(loc, other)
} else if (loc.locType === 'node' && isID(other)) {
return equal(loc, other)
} else if (isNode(other) || isRootNode(other) || isPort(other)) {
return identifiesNode(loc, other)
} else {
throw new Error('Unable to identify object type. Checking for: ' + JSON.stringify(loc) + ' object is: ' + JSON.stringify(other))
}
})
export const toString = (loc) => {
return JSON.stringify(loc)
}