08-27-周三_17-09-29
This commit is contained in:
53
node_modules/dagre-layout/lib/order/add-subgraph-constraints.js
generated
vendored
Normal file
53
node_modules/dagre-layout/lib/order/add-subgraph-constraints.js
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
function addSubgraphConstraints (g, cg, vs) {
|
||||
const prev = {}
|
||||
let rootPrev
|
||||
|
||||
_.forEach(vs, function (v) {
|
||||
let child = g.parent(v)
|
||||
let parent
|
||||
let prevChild
|
||||
while (child) {
|
||||
parent = g.parent(child)
|
||||
if (parent) {
|
||||
prevChild = prev[parent]
|
||||
prev[parent] = child
|
||||
} else {
|
||||
prevChild = rootPrev
|
||||
rootPrev = child
|
||||
}
|
||||
if (prevChild && prevChild !== child) {
|
||||
cg.setEdge(prevChild, child)
|
||||
return
|
||||
}
|
||||
child = parent
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
function dfs(v) {
|
||||
const children = v ? g.children(v) : g.children();
|
||||
if (children.length) {
|
||||
const min = Number.POSITIVE_INFINITY,
|
||||
subgraphs = [];
|
||||
_.forEach(children, function(child) {
|
||||
const childMin = dfs(child);
|
||||
if (g.children(child).length) {
|
||||
subgraphs.push({ v: child, order: childMin });
|
||||
}
|
||||
min = Math.min(min, childMin);
|
||||
});
|
||||
_.reduce(_.sortBy(subgraphs, "order"), function(prev, curr) {
|
||||
cg.setEdge(prev.v, curr.v);
|
||||
return curr;
|
||||
});
|
||||
return min;
|
||||
}
|
||||
return g.node(v).order;
|
||||
}
|
||||
dfs(undefined);
|
||||
*/
|
||||
}
|
||||
|
||||
export default addSubgraphConstraints
|
27
node_modules/dagre-layout/lib/order/barycenter.js
generated
vendored
Normal file
27
node_modules/dagre-layout/lib/order/barycenter.js
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
function barycenter (g, movable) {
|
||||
return _.map(movable, function (v) {
|
||||
const inV = g.inEdges(v)
|
||||
if (!inV.length) {
|
||||
return { v: v }
|
||||
} else {
|
||||
const result = _.reduce(inV, function (acc, e) {
|
||||
const edge = g.edge(e)
|
||||
const nodeU = g.node(e.v)
|
||||
return {
|
||||
sum: acc.sum + (edge.weight * nodeU.order),
|
||||
weight: acc.weight + edge.weight
|
||||
}
|
||||
}, { sum: 0, weight: 0 })
|
||||
|
||||
return {
|
||||
v: v,
|
||||
barycenter: result.sum / result.weight,
|
||||
weight: result.weight
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default barycenter
|
73
node_modules/dagre-layout/lib/order/build-layer-graph.js
generated
vendored
Normal file
73
node_modules/dagre-layout/lib/order/build-layer-graph.js
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
import _ from 'lodash'
|
||||
import { Graph } from 'graphlibrary'
|
||||
|
||||
/*
|
||||
* Constructs a graph that can be used to sort a layer of nodes. The graph will
|
||||
* contain all base and subgraph nodes from the request layer in their original
|
||||
* hierarchy and any edges that are incident on these nodes and are of the type
|
||||
* requested by the "relationship" parameter.
|
||||
*
|
||||
* Nodes from the requested rank that do not have parents are assigned a root
|
||||
* node in the output graph, which is set in the root graph attribute. This
|
||||
* makes it easy to walk the hierarchy of movable nodes during ordering.
|
||||
*
|
||||
* Pre-conditions:
|
||||
*
|
||||
* 1. Input graph is a DAG
|
||||
* 2. Base nodes in the input graph have a rank attribute
|
||||
* 3. Subgraph nodes in the input graph has minRank and maxRank attributes
|
||||
* 4. Edges have an assigned weight
|
||||
*
|
||||
* Post-conditions:
|
||||
*
|
||||
* 1. Output graph has all nodes in the movable rank with preserved
|
||||
* hierarchy.
|
||||
* 2. Root nodes in the movable layer are made children of the node
|
||||
* indicated by the root attribute of the graph.
|
||||
* 3. Non-movable nodes incident on movable nodes, selected by the
|
||||
* relationship parameter, are included in the graph (without hierarchy).
|
||||
* 4. Edges incident on movable nodes, selected by the relationship
|
||||
* parameter, are added to the output graph.
|
||||
* 5. The weights for copied edges are aggregated as need, since the output
|
||||
* graph is not a multi-graph.
|
||||
*/
|
||||
function buildLayerGraph (g, rank, relationship) {
|
||||
const root = createRootNode(g)
|
||||
const result = new Graph({ compound: true }).setGraph({ root: root })
|
||||
.setDefaultNodeLabel(function (v) { return g.node(v) })
|
||||
|
||||
_.forEach(g.nodes(), function (v) {
|
||||
const node = g.node(v)
|
||||
const parent = g.parent(v)
|
||||
|
||||
if (node.rank === rank || (node.minRank <= rank && rank <= node.maxRank)) {
|
||||
result.setNode(v)
|
||||
result.setParent(v, parent || root)
|
||||
|
||||
// This assumes we have only short edges!
|
||||
_.forEach(g[relationship](v), function (e) {
|
||||
const u = e.v === v ? e.w : e.v
|
||||
const edge = result.edge(u, v)
|
||||
const weight = !_.isUndefined(edge) ? edge.weight : 0
|
||||
result.setEdge(u, v, { weight: g.edge(e).weight + weight })
|
||||
})
|
||||
|
||||
if (_.has(node, 'minRank')) {
|
||||
result.setNode(v, {
|
||||
borderLeft: node.borderLeft[rank],
|
||||
borderRight: node.borderRight[rank]
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function createRootNode (g) {
|
||||
let v
|
||||
while (g.hasNode((v = _.uniqueId('_root'))));
|
||||
return v
|
||||
}
|
||||
|
||||
export default buildLayerGraph
|
70
node_modules/dagre-layout/lib/order/cross-count.js
generated
vendored
Normal file
70
node_modules/dagre-layout/lib/order/cross-count.js
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
/*
|
||||
* A function that takes a layering (an array of layers, each with an array of
|
||||
* ordererd nodes) and a graph and returns a weighted crossing count.
|
||||
*
|
||||
* Pre-conditions:
|
||||
*
|
||||
* 1. Input graph must be simple (not a multigraph), directed, and include
|
||||
* only simple edges.
|
||||
* 2. Edges in the input graph must have assigned weights.
|
||||
*
|
||||
* Post-conditions:
|
||||
*
|
||||
* 1. The graph and layering matrix are left unchanged.
|
||||
*
|
||||
* This algorithm is derived from Barth, et al., "Bilayer Cross Counting."
|
||||
*/
|
||||
function crossCount (g, layering) {
|
||||
let cc = 0
|
||||
for (let i = 1; i < layering.length; ++i) {
|
||||
cc += twoLayerCrossCount(g, layering[i - 1], layering[i])
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
function twoLayerCrossCount (g, northLayer, southLayer) {
|
||||
// Sort all of the edges between the north and south layers by their position
|
||||
// in the north layer and then the south. Map these edges to the position of
|
||||
// their head in the south layer.
|
||||
const southPos = _.zipObject(southLayer,
|
||||
_.map(southLayer, function (v, i) { return i }))
|
||||
const southEntries = _.flatten(_.map(northLayer, function (v) {
|
||||
return _.chain(g.outEdges(v))
|
||||
.map(function (e) {
|
||||
return { pos: southPos[e.w], weight: g.edge(e).weight }
|
||||
})
|
||||
.sortBy('pos')
|
||||
.value()
|
||||
}), true)
|
||||
|
||||
// Build the accumulator tree
|
||||
let firstIndex = 1
|
||||
while (firstIndex < southLayer.length) {
|
||||
firstIndex <<= 1
|
||||
}
|
||||
const treeSize = 2 * firstIndex - 1
|
||||
firstIndex -= 1
|
||||
const tree = _.map(new Array(treeSize), function () { return 0 })
|
||||
|
||||
// Calculate the weighted crossings
|
||||
let cc = 0
|
||||
_.forEach(southEntries.forEach(function (entry) {
|
||||
let index = entry.pos + firstIndex
|
||||
tree[index] += entry.weight
|
||||
let weightSum = 0
|
||||
while (index > 0) {
|
||||
if (index % 2) {
|
||||
weightSum += tree[index + 1]
|
||||
}
|
||||
index = (index - 1) >> 1
|
||||
tree[index] += entry.weight
|
||||
}
|
||||
cc += entry.weight * weightSum
|
||||
}))
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
export default crossCount
|
78
node_modules/dagre-layout/lib/order/index.js
generated
vendored
Normal file
78
node_modules/dagre-layout/lib/order/index.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
import _ from 'lodash'
|
||||
import { Graph } from 'graphlibrary'
|
||||
|
||||
import initOrder from './init-order'
|
||||
import crossCount from './cross-count'
|
||||
import sortSubgraph from './sort-subgraph'
|
||||
import buildLayerGraph from './build-layer-graph'
|
||||
import addSubgraphConstraints from './add-subgraph-constraints'
|
||||
import util from '../util'
|
||||
|
||||
/*
|
||||
* Applies heuristics to minimize edge crossings in the graph and sets the best
|
||||
* order solution as an order attribute on each node.
|
||||
*
|
||||
* Pre-conditions:
|
||||
*
|
||||
* 1. Graph must be DAG
|
||||
* 2. Graph nodes must be objects with a "rank" attribute
|
||||
* 3. Graph edges must have the "weight" attribute
|
||||
*
|
||||
* Post-conditions:
|
||||
*
|
||||
* 1. Graph nodes will have an "order" attribute based on the results of the
|
||||
* algorithm.
|
||||
*/
|
||||
function order (g) {
|
||||
const maxRank = util.maxRank(g)
|
||||
const downLayerGraphs = buildLayerGraphs(g, _.range(1, maxRank + 1), 'inEdges')
|
||||
const upLayerGraphs = buildLayerGraphs(g, _.range(maxRank - 1, -1, -1), 'outEdges')
|
||||
|
||||
let layering = initOrder(g)
|
||||
assignOrder(g, layering)
|
||||
|
||||
let bestCC = Number.POSITIVE_INFINITY
|
||||
let best
|
||||
|
||||
for (let i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) {
|
||||
sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2)
|
||||
|
||||
layering = util.buildLayerMatrix(g)
|
||||
const cc = crossCount(g, layering)
|
||||
if (cc < bestCC) {
|
||||
lastBest = 0
|
||||
best = _.cloneDeep(layering)
|
||||
bestCC = cc
|
||||
}
|
||||
}
|
||||
|
||||
assignOrder(g, best)
|
||||
}
|
||||
|
||||
function buildLayerGraphs (g, ranks, relationship) {
|
||||
return _.map(ranks, function (rank) {
|
||||
return buildLayerGraph(g, rank, relationship)
|
||||
})
|
||||
}
|
||||
|
||||
function sweepLayerGraphs (layerGraphs, biasRight) {
|
||||
const cg = new Graph()
|
||||
_.forEach(layerGraphs, function (lg) {
|
||||
const root = lg.graph().root
|
||||
const sorted = sortSubgraph(lg, root, cg, biasRight)
|
||||
_.forEach(sorted.vs, function (v, i) {
|
||||
lg.node(v).order = i
|
||||
})
|
||||
addSubgraphConstraints(lg, cg, sorted.vs)
|
||||
})
|
||||
}
|
||||
|
||||
function assignOrder (g, layering) {
|
||||
_.forEach(layering, function (layer) {
|
||||
_.forEach(layer, function (v, i) {
|
||||
g.node(v).order = i
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default order
|
36
node_modules/dagre-layout/lib/order/init-order.js
generated
vendored
Normal file
36
node_modules/dagre-layout/lib/order/init-order.js
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
/*
|
||||
* Assigns an initial order value for each node by performing a DFS search
|
||||
* starting from nodes in the first rank. Nodes are assigned an order in their
|
||||
* rank as they are first visited.
|
||||
*
|
||||
* This approach comes from Gansner, et al., "A Technique for Drawing Directed
|
||||
* Graphs."
|
||||
*
|
||||
* Returns a layering matrix with an array per layer and each layer sorted by
|
||||
* the order of its nodes.
|
||||
*/
|
||||
function initOrder (g) {
|
||||
const visited = {}
|
||||
const simpleNodes = _.filter(g.nodes(), function (v) {
|
||||
return !g.children(v).length
|
||||
})
|
||||
const maxRank = _.max(_.map(simpleNodes, function (v) { return g.node(v).rank }))
|
||||
const layers = _.map(_.range(maxRank + 1), function () { return [] })
|
||||
|
||||
function dfs (v) {
|
||||
if (_.has(visited, v)) return
|
||||
visited[v] = true
|
||||
const node = g.node(v)
|
||||
layers[node.rank].push(v)
|
||||
_.forEach(g.successors(v), dfs)
|
||||
}
|
||||
|
||||
const orderedVs = _.sortBy(simpleNodes, function (v) { return g.node(v).rank })
|
||||
_.forEach(orderedVs, dfs)
|
||||
|
||||
return layers
|
||||
}
|
||||
|
||||
export default initOrder
|
121
node_modules/dagre-layout/lib/order/resolve-conflicts.js
generated
vendored
Normal file
121
node_modules/dagre-layout/lib/order/resolve-conflicts.js
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
/*
|
||||
* Given a list of entries of the form {v, barycenter, weight} and a
|
||||
* constraint graph this function will resolve any conflicts between the
|
||||
* constraint graph and the barycenters for the entries. If the barycenters for
|
||||
* an entry would violate a constraint in the constraint graph then we coalesce
|
||||
* the nodes in the conflict into a new node that respects the contraint and
|
||||
* aggregates barycenter and weight information.
|
||||
*
|
||||
* This implementation is based on the description in Forster, "A Fast and
|
||||
* Simple Hueristic for Constrained Two-Level Crossing Reduction," thought it
|
||||
* differs in some specific details.
|
||||
*
|
||||
* Pre-conditions:
|
||||
*
|
||||
* 1. Each entry has the form {v, barycenter, weight}, or if the node has
|
||||
* no barycenter, then {v}.
|
||||
*
|
||||
* Returns:
|
||||
*
|
||||
* A new list of entries of the form {vs, i, barycenter, weight}. The list
|
||||
* `vs` may either be a singleton or it may be an aggregation of nodes
|
||||
* ordered such that they do not violate constraints from the constraint
|
||||
* graph. The property `i` is the lowest original index of any of the
|
||||
* elements in `vs`.
|
||||
*/
|
||||
function resolveConflicts (entries, cg) {
|
||||
const mappedEntries = {}
|
||||
_.forEach(entries, function (entry, i) {
|
||||
const tmp = mappedEntries[entry.v] = {
|
||||
indegree: 0,
|
||||
'in': [],
|
||||
out: [],
|
||||
vs: [entry.v],
|
||||
i: i
|
||||
}
|
||||
if (!_.isUndefined(entry.barycenter)) {
|
||||
tmp.barycenter = entry.barycenter
|
||||
tmp.weight = entry.weight
|
||||
}
|
||||
})
|
||||
|
||||
_.forEach(cg.edges(), function (e) {
|
||||
const entryV = mappedEntries[e.v]
|
||||
const entryW = mappedEntries[e.w]
|
||||
if (!_.isUndefined(entryV) && !_.isUndefined(entryW)) {
|
||||
entryW.indegree++
|
||||
entryV.out.push(mappedEntries[e.w])
|
||||
}
|
||||
})
|
||||
|
||||
const sourceSet = _.filter(mappedEntries, function (entry) {
|
||||
return !entry.indegree
|
||||
})
|
||||
|
||||
return doResolveConflicts(sourceSet)
|
||||
}
|
||||
|
||||
function doResolveConflicts (sourceSet) {
|
||||
const entries = []
|
||||
|
||||
function handleIn (vEntry) {
|
||||
return function (uEntry) {
|
||||
if (uEntry.merged) {
|
||||
return
|
||||
}
|
||||
if (_.isUndefined(uEntry.barycenter) ||
|
||||
_.isUndefined(vEntry.barycenter) ||
|
||||
uEntry.barycenter >= vEntry.barycenter) {
|
||||
mergeEntries(vEntry, uEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleOut (vEntry) {
|
||||
return function (wEntry) {
|
||||
wEntry['in'].push(vEntry)
|
||||
if (--wEntry.indegree === 0) {
|
||||
sourceSet.push(wEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (sourceSet.length) {
|
||||
const entry = sourceSet.pop()
|
||||
entries.push(entry)
|
||||
_.forEach(entry['in'].reverse(), handleIn(entry))
|
||||
_.forEach(entry.out, handleOut(entry))
|
||||
}
|
||||
|
||||
return _.chain(entries)
|
||||
.filter(function (entry) { return !entry.merged })
|
||||
.map(function (entry) {
|
||||
return _.pick(entry, ['vs', 'i', 'barycenter', 'weight'])
|
||||
})
|
||||
.value()
|
||||
}
|
||||
|
||||
function mergeEntries (target, source) {
|
||||
let sum = 0
|
||||
let weight = 0
|
||||
|
||||
if (target.weight) {
|
||||
sum += target.barycenter * target.weight
|
||||
weight += target.weight
|
||||
}
|
||||
|
||||
if (source.weight) {
|
||||
sum += source.barycenter * source.weight
|
||||
weight += source.weight
|
||||
}
|
||||
|
||||
target.vs = source.vs.concat(target.vs)
|
||||
target.barycenter = sum / weight
|
||||
target.weight = weight
|
||||
target.i = Math.min(source.i, target.i)
|
||||
source.merged = true
|
||||
}
|
||||
|
||||
export default resolveConflicts
|
77
node_modules/dagre-layout/lib/order/sort-subgraph.js
generated
vendored
Normal file
77
node_modules/dagre-layout/lib/order/sort-subgraph.js
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import barycenter from './barycenter'
|
||||
import resolveConflicts from './resolve-conflicts'
|
||||
import sort from './sort'
|
||||
|
||||
function sortSubgraph (g, v, cg, biasRight) {
|
||||
let movable = g.children(v)
|
||||
const node = g.node(v)
|
||||
const bl = node ? node.borderLeft : undefined
|
||||
const br = node ? node.borderRight : undefined
|
||||
const subgraphs = {}
|
||||
|
||||
if (bl) {
|
||||
movable = _.filter(movable, function (w) {
|
||||
return w !== bl && w !== br
|
||||
})
|
||||
}
|
||||
|
||||
const barycenters = barycenter(g, movable)
|
||||
_.forEach(barycenters, function (entry) {
|
||||
if (g.children(entry.v).length) {
|
||||
const subgraphResult = sortSubgraph(g, entry.v, cg, biasRight)
|
||||
subgraphs[entry.v] = subgraphResult
|
||||
if (_.has(subgraphResult, 'barycenter')) {
|
||||
mergeBarycenters(entry, subgraphResult)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const entries = resolveConflicts(barycenters, cg)
|
||||
expandSubgraphs(entries, subgraphs)
|
||||
|
||||
const result = sort(entries, biasRight)
|
||||
|
||||
if (bl) {
|
||||
result.vs = _.flatten([bl, result.vs, br], true)
|
||||
if (g.predecessors(bl).length) {
|
||||
const blPred = g.node(g.predecessors(bl)[0])
|
||||
const brPred = g.node(g.predecessors(br)[0])
|
||||
if (!_.has(result, 'barycenter')) {
|
||||
result.barycenter = 0
|
||||
result.weight = 0
|
||||
}
|
||||
result.barycenter = (result.barycenter * result.weight +
|
||||
blPred.order + brPred.order) / (result.weight + 2)
|
||||
result.weight += 2
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function expandSubgraphs (entries, subgraphs) {
|
||||
_.forEach(entries, function (entry) {
|
||||
entry.vs = _.flatten(entry.vs.map(function (v) {
|
||||
if (subgraphs[v]) {
|
||||
return subgraphs[v].vs
|
||||
}
|
||||
return v
|
||||
}), true)
|
||||
})
|
||||
}
|
||||
|
||||
function mergeBarycenters (target, other) {
|
||||
if (!_.isUndefined(target.barycenter)) {
|
||||
target.barycenter = (target.barycenter * target.weight +
|
||||
other.barycenter * other.weight) /
|
||||
(target.weight + other.weight)
|
||||
target.weight += other.weight
|
||||
} else {
|
||||
target.barycenter = other.barycenter
|
||||
target.weight = other.weight
|
||||
}
|
||||
}
|
||||
|
||||
export default sortSubgraph
|
58
node_modules/dagre-layout/lib/order/sort.js
generated
vendored
Normal file
58
node_modules/dagre-layout/lib/order/sort.js
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
import _ from 'lodash'
|
||||
|
||||
import util from '../util'
|
||||
|
||||
function sort (entries, biasRight) {
|
||||
const parts = util.partition(entries, function (entry) {
|
||||
return _.has(entry, 'barycenter')
|
||||
})
|
||||
const sortable = parts.lhs
|
||||
const unsortable = _.sortBy(parts.rhs, function (entry) { return -entry.i })
|
||||
const vs = []
|
||||
let sum = 0
|
||||
let weight = 0
|
||||
let vsIndex = 0
|
||||
|
||||
sortable.sort(compareWithBias(!!biasRight))
|
||||
|
||||
vsIndex = consumeUnsortable(vs, unsortable, vsIndex)
|
||||
|
||||
_.forEach(sortable, function (entry) {
|
||||
vsIndex += entry.vs.length
|
||||
vs.push(entry.vs)
|
||||
sum += entry.barycenter * entry.weight
|
||||
weight += entry.weight
|
||||
vsIndex = consumeUnsortable(vs, unsortable, vsIndex)
|
||||
})
|
||||
|
||||
const result = { vs: _.flatten(vs, true) }
|
||||
if (weight) {
|
||||
result.barycenter = sum / weight
|
||||
result.weight = weight
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function consumeUnsortable (vs, unsortable, index) {
|
||||
let last
|
||||
while (unsortable.length && (last = _.last(unsortable)).i <= index) {
|
||||
unsortable.pop()
|
||||
vs.push(last.vs)
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
function compareWithBias (bias) {
|
||||
return function (entryV, entryW) {
|
||||
if (entryV.barycenter < entryW.barycenter) {
|
||||
return -1
|
||||
} else if (entryV.barycenter > entryW.barycenter) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return !bias ? entryV.i - entryW.i : entryW.i - entryV.i
|
||||
}
|
||||
}
|
||||
|
||||
export default sort
|
Reference in New Issue
Block a user