+
+ 📥
-
+
-
+
{{ processLabel }}
diff --git a/docs/examples/layout/index.ts b/docs/examples/layout/index.ts
index 75b3a14c..50364667 100644
--- a/docs/examples/layout/index.ts
+++ b/docs/examples/layout/index.ts
@@ -3,6 +3,5 @@ export { default as LayoutElements } from './initial-elements.js?raw'
export { default as LayoutNode } from './ProcessNode.vue?raw'
export { default as LayoutEdge } from './AnimationEdge.vue?raw'
export { default as useRunProcess } from './useRunProcess.js?raw'
-export { default as useShuffle } from './useShuffle.js?raw'
export { default as useLayout } from './useLayout.js?raw'
export { default as LayoutIcon } from './Icon.vue?raw'
diff --git a/docs/examples/layout/initial-elements.js b/docs/examples/layout/initial-elements.js
index c83083c0..f8c73ddc 100644
--- a/docs/examples/layout/initial-elements.js
+++ b/docs/examples/layout/initial-elements.js
@@ -1,61 +1,73 @@
-const position = { x: 0, y: 0 }
-const nodeType = 'process'
+const initialPos = { x: 0, y: 0 }
+const type = 'process'
+const data = { status: null }
export const initialNodes = [
{
id: '1',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '2',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '2a',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '2b',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '2c',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '2d',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '3',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '4',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '5',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '6',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
{
id: '7',
- position,
- type: nodeType,
+ position: initialPos,
+ type,
+ data,
},
]
diff --git a/docs/examples/layout/useLayout.js b/docs/examples/layout/useLayout.js
index 652aced6..34714565 100644
--- a/docs/examples/layout/useLayout.js
+++ b/docs/examples/layout/useLayout.js
@@ -5,14 +5,14 @@ import { ref } from 'vue'
/**
* Composable to run the layout algorithm on the graph.
* It uses the `dagre` library to calculate the layout of the nodes and edges.
+ *
+ * @see https://github.com/dagrejs/dagre/wiki
*/
export function useLayout() {
const { findNode } = useVueFlow()
const graph = ref(new dagre.graphlib.Graph())
- const previousDirection = ref('LR')
-
function layout(nodes, edges, direction) {
// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
const dagreGraph = new dagre.graphlib.Graph()
@@ -24,12 +24,15 @@ export function useLayout() {
const isHorizontal = direction === 'LR'
dagreGraph.setGraph({ rankdir: direction })
- previousDirection.value = direction
-
for (const node of nodes) {
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
const graphNode = findNode(node.id)
+ if (!graphNode) {
+ console.error(`Node with id ${node.id} not found in the graph`)
+ continue
+ }
+
dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
}
@@ -52,5 +55,5 @@ export function useLayout() {
})
}
- return { graph, layout, previousDirection }
+ return { graph, layout }
}
diff --git a/docs/examples/layout/useRunProcess.js b/docs/examples/layout/useRunProcess.js
index 635958f3..bb4ab886 100644
--- a/docs/examples/layout/useRunProcess.js
+++ b/docs/examples/layout/useRunProcess.js
@@ -1,6 +1,14 @@
import { ref, toRef, toValue } from 'vue'
import { useVueFlow } from '@vue-flow/core'
+export const ProcessStatus = {
+ ERROR: 'error',
+ SKIPPED: 'skipped',
+ CANCELLED: 'cancelled',
+ FINISHED: 'finished',
+ RUNNING: 'running',
+}
+
/**
* Composable to simulate running a process tree.
*
@@ -8,6 +16,10 @@ import { useVueFlow } from '@vue-flow/core'
* When one node finishes, the next one starts.
*
* When a node has multiple descendants, it will run them in parallel.
+ *
+ * @param options
+ * @param options.graph The graph object containing the nodes and edges.
+ * @param options.cancelOnError Whether to cancel the process if an error occurs.
*/
export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
const { updateNodeData, getConnectedEdges } = useVueFlow()
@@ -16,24 +28,35 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
const isRunning = ref(false)
- const executedNodes = new Set()
-
+ /** Map of running tasks with the node ID as the key and the timeout as the value. */
const runningTasks = new Map()
+ /** Set of node ids of nodes that have been executed */
+ const executedNodes = new Set()
+
+ /** Set of node ids yet to be executed */
const upcomingTasks = new Set()
- async function runNode(node, isStart = false) {
- if (executedNodes.has(node.id)) {
+ /**
+ * Run the process on a node.
+ * It will mark the node as running, simulate an async process, and then mark the node as finished or errored.
+ *
+ * @param nodeId The ID of the node to run.
+ * @param isStart Whether this is a starting node.
+ */
+ async function runNode(nodeId, isStart = false) {
+ if (executedNodes.has(nodeId)) {
return
}
// save the upcoming task in case it gets cancelled before we even start it
- upcomingTasks.add(node.id)
+ upcomingTasks.add(nodeId)
- const incomers = getConnectedEdges(node.id).filter((connection) => connection.target === node.id)
+ // get all incoming edges to this node
+ const incomers = getConnectedEdges(nodeId).filter((connection) => connection.target === nodeId)
// wait for edge animations to finish before starting the process
- await Promise.all(incomers.map((incomer) => until(() => !incomer.data.isAnimating)))
+ await Promise.all(incomers.map((incomer) => until(() => !incomer.data?.isAnimating)))
// remove the upcoming task since we are about to start it
upcomingTasks.clear()
@@ -44,9 +67,9 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
}
// mark the node as executed, so it doesn't run again
- executedNodes.add(node.id)
+ executedNodes.add(nodeId)
- updateNodeData(node.id, { isRunning: true, isFinished: false, hasError: false, isCancelled: false })
+ updateNodeStatus(nodeId, ProcessStatus.RUNNING)
// simulate an async process with a random timeout between 1-2 seconds
const delay = Math.floor(Math.random() * 2000) + 1000
@@ -54,49 +77,63 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
return new Promise((resolve) => {
const timeout = setTimeout(
async () => {
- const children = graph.value.successors(node.id)
+ // get all children of this node
+ const children = graph.value.successors(nodeId) || []
// randomly decide whether the node will throw an error
const willThrowError = Math.random() < 0.15
// we avoid throwing an error on the starting node
if (!isStart && willThrowError) {
- updateNodeData(node.id, { isRunning: false, hasError: true })
+ updateNodeStatus(nodeId, ProcessStatus.ERROR)
+ // if cancelOnError is true, we stop the process and mark all descendants as skipped
if (toValue(cancelOnError)) {
- await skipDescendants(node.id)
- runningTasks.delete(node.id)
+ await skipDescendants(nodeId)
+ runningTasks.delete(nodeId)
- resolve()
+ resolve(true)
return
}
}
- updateNodeData(node.id, { isRunning: false, isFinished: true })
+ updateNodeStatus(nodeId, ProcessStatus.FINISHED)
- runningTasks.delete(node.id)
+ runningTasks.delete(nodeId)
if (children.length > 0) {
// run the process on the children in parallel
- await Promise.all(children.map((id) => runNode({ id })))
+ await Promise.all(children.map((child) => runNode(child)))
}
- resolve()
+ resolve(true)
},
// if this is a starting node, we don't want to wait
isStart ? 0 : delay,
)
// save the timeout so we can cancel it if needed
- runningTasks.set(node.id, timeout)
+ runningTasks.set(nodeId, timeout)
})
}
+ /**
+ * Run a sequence of nodes.
+ * It will start with the nodes that have no predecessors and then run the process on each node in sequence.
+ * If a node has multiple descendants, it will run them in parallel.
+ * If an error occurs, it will stop the process and mark all descendants as skipped.
+ * If cancelOnError is true, it will stop the process if an error occurs.
+ * If the process is stopped, it will mark all running nodes as cancelled.
+ *
+ * @param nodes The nodes to run.
+ */
async function run(nodes) {
+ // if the process is already running, we don't want to start it again
if (isRunning.value) {
return
}
+ // reset all nodes to their initial state
reset(nodes)
isRunning.value = true
@@ -105,42 +142,57 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
const startingNodes = nodes.filter((node) => graph.value.predecessors(node.id)?.length === 0)
// run the process on all starting nodes in parallel
- await Promise.all(startingNodes.map((node) => runNode(node, true)))
+ await Promise.all(startingNodes.map((node) => runNode(node.id, true)))
clear()
}
+ /**
+ * Reset all nodes to their initial state.
+ *
+ * @param nodes The nodes to reset.
+ */
function reset(nodes) {
clear()
for (const node of nodes) {
- updateNodeData(node.id, { isRunning: false, isFinished: false, hasError: false, isSkipped: false, isCancelled: false })
+ updateNodeStatus(node.id, null)
}
}
+ /**
+ * Skip all descendants of a node.
+ *
+ * @param nodeId The ID of the node to skip descendants for.
+ */
async function skipDescendants(nodeId) {
- const children = graph.value.successors(nodeId)
+ const children = graph.value.successors(nodeId) || []
for (const child of children) {
- updateNodeData(child, { isRunning: false, isSkipped: true })
+ updateNodeStatus(child, ProcessStatus.SKIPPED)
await skipDescendants(child)
}
}
+ /**
+ * Stop the process.
+ *
+ * It will mark all running nodes as cancelled and skip all upcoming tasks.
+ */
async function stop() {
isRunning.value = false
for (const nodeId of upcomingTasks) {
clearTimeout(runningTasks.get(nodeId))
runningTasks.delete(nodeId)
- updateNodeData(nodeId, { isRunning: false, isFinished: false, hasError: false, isSkipped: false, isCancelled: true })
+ updateNodeStatus(nodeId, ProcessStatus.CANCELLED)
await skipDescendants(nodeId)
}
for (const [nodeId, task] of runningTasks) {
clearTimeout(task)
runningTasks.delete(nodeId)
- updateNodeData(nodeId, { isRunning: false, isFinished: false, hasError: false, isSkipped: false, isCancelled: true })
+ updateNodeStatus(nodeId, ProcessStatus.CANCELLED)
await skipDescendants(nodeId)
}
@@ -148,12 +200,25 @@ export function useRunProcess({ graph: dagreGraph, cancelOnError = true }) {
upcomingTasks.clear()
}
+ /**
+ * Clear all running tasks and executed nodes.
+ */
function clear() {
isRunning.value = false
executedNodes.clear()
runningTasks.clear()
}
+ /**
+ * Update the status of a node.
+ *
+ * @param nodeId The ID of the node to update.
+ * @param status The new status of the node.
+ */
+ function updateNodeStatus(nodeId, status) {
+ updateNodeData(nodeId, { status })
+ }
+
return { run, stop, reset, isRunning }
}
@@ -162,7 +227,7 @@ async function until(condition) {
const interval = setInterval(() => {
if (condition()) {
clearInterval(interval)
- resolve()
+ resolve(true)
}
}, 100)
})
diff --git a/docs/examples/layout/useShuffle.js b/docs/examples/layout/useShuffle.js
deleted file mode 100644
index ac7e51bc..00000000
--- a/docs/examples/layout/useShuffle.js
+++ /dev/null
@@ -1,48 +0,0 @@
-function shuffleArray(array) {
- for (let i = array.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1))
- ;[array[i], array[j]] = [array[j], array[i]]
- }
-}
-
-// generate a list of all possible edges, excluding duplicates and self-references
-function generatePossibleEdges(nodes) {
- const possibleEdges = []
-
- for (const sourceNode of nodes) {
- for (const targetNode of nodes) {
- if (sourceNode.id !== targetNode.id) {
- const edgeId = `e${sourceNode.id}-${targetNode.id}`
- possibleEdges.push({ id: edgeId, source: sourceNode.id, target: targetNode.id, type: 'animation', animated: true })
- }
- }
- }
-
- return possibleEdges
-}
-
-/**
- * Composable that returns a function to shuffle the edges of a graph
- *
- * @returns {Function} A function that takes a list of nodes and returns a list of edges
- */
-export function useShuffle() {
- return (nodes) => {
- const possibleEdges = generatePossibleEdges(nodes)
- shuffleArray(possibleEdges)
-
- const usedNodes = new Set()
- const newEdges = []
-
- // ensure every node is used, maintaining a single starting node and no duplicate connections
- for (const edge of possibleEdges) {
- if (!usedNodes.has(edge.target) && (usedNodes.size === 0 || usedNodes.has(edge.source))) {
- newEdges.push(edge)
- usedNodes.add(edge.source)
- usedNodes.add(edge.target)
- }
- }
-
- return newEdges
- }
-}
diff --git a/examples/vite/src/Layouting/AnimationEdge.vue b/examples/vite/src/Layouting/AnimationEdge.vue
deleted file mode 100644
index f060ffac..00000000
--- a/examples/vite/src/Layouting/AnimationEdge.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/vite/src/Layouting/components/Icon.vue b/examples/vite/src/Layouting/components/Icon.vue
new file mode 100644
index 00000000..28274952
--- /dev/null
+++ b/examples/vite/src/Layouting/components/Icon.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/vite/src/Layouting/composables/useLayout.ts b/examples/vite/src/Layouting/composables/useLayout.ts
new file mode 100644
index 00000000..f6c0d228
--- /dev/null
+++ b/examples/vite/src/Layouting/composables/useLayout.ts
@@ -0,0 +1,60 @@
+import dagre from '@dagrejs/dagre'
+import type { Edge, Node } from '@vue-flow/core'
+import { Position, useVueFlow } from '@vue-flow/core'
+import { ref } from 'vue'
+
+export type Direction = 'LR' | 'TB'
+
+/**
+ * Composable to run the layout algorithm on the graph.
+ * It uses the `dagre` library to calculate the layout of the nodes and edges.
+ */
+export function useLayout() {
+ const { findNode } = useVueFlow()
+
+ const graph = ref(new dagre.graphlib.Graph())
+
+ function layout(nodes: NodeType[], edges: EdgeType[], direction: Direction) {
+ // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
+ const dagreGraph = new dagre.graphlib.Graph()
+
+ graph.value = dagreGraph
+
+ dagreGraph.setDefaultEdgeLabel(() => ({}))
+
+ const isHorizontal = direction === 'LR'
+ dagreGraph.setGraph({ rankdir: direction })
+
+ for (const node of nodes) {
+ // if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
+ const graphNode = findNode(node.id)
+
+ if (!graphNode) {
+ console.error(`Node with id ${node.id} not found in the graph`)
+ continue
+ }
+
+ dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
+ }
+
+ for (const edge of edges) {
+ dagreGraph.setEdge(edge.source, edge.target)
+ }
+
+ dagre.layout(dagreGraph)
+
+ // set nodes with updated positions
+ return nodes.map((node) => {
+ const nodeWithPosition = dagreGraph.node(node.id)
+
+ return {
+ ...node,
+ targetPosition: isHorizontal ? Position.Left : Position.Top,
+ sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
+ position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
+ }
+ })
+ }
+
+ return { graph, layout }
+}
diff --git a/examples/vite/src/Layouting/composables/useRunProcess.ts b/examples/vite/src/Layouting/composables/useRunProcess.ts
new file mode 100644
index 00000000..44c1426b
--- /dev/null
+++ b/examples/vite/src/Layouting/composables/useRunProcess.ts
@@ -0,0 +1,234 @@
+import { ref, toRef, toValue } from 'vue'
+import type { Node } from '@vue-flow/core'
+import { useVueFlow } from '@vue-flow/core'
+import type { MaybeRefOrGetter } from 'vue'
+import type dagre from '@dagrejs/dagre'
+import type { ProcessData, ProcessNode } from '../nodes'
+import { ProcessStatus } from '../nodes'
+import type { ProcessEdge } from '../edges'
+
+interface UseRunProcessOptions {
+ graph: MaybeRefOrGetter>
+ cancelOnError?: MaybeRefOrGetter
+}
+
+/**
+ * Composable to simulate running a process tree.
+ *
+ * It loops through each node, pretends to run an async process, and updates the node's data indicating whether the process has finished.
+ * When one node finishes, the next one starts.
+ *
+ * When a node has multiple descendants, it will run them in parallel.
+ *
+ * @param options
+ * @param options.graph The graph object containing the nodes and edges.
+ * @param options.cancelOnError Whether to cancel the process if an error occurs.
+ */
+export function useRunProcess({ graph: dagreGraph, cancelOnError = true }: UseRunProcessOptions) {
+ const { updateNodeData, getConnectedEdges } = useVueFlow()
+
+ const graph = toRef(() => toValue(dagreGraph))
+
+ const isRunning = ref(false)
+
+ const runningTasks = new Map()
+
+ const executedNodes = new Set()
+
+ const upcomingTasks = new Set()
+
+ /**
+ * Run the process on a node.
+ * It will mark the node as running, simulate an async process, and then mark the node as finished or errored.
+ *
+ * @param nodeId The ID of the node to run.
+ * @param isStart Whether this is a starting node.
+ */
+ async function runNode(nodeId: string, isStart = false) {
+ if (executedNodes.has(nodeId)) {
+ return
+ }
+
+ // save the upcoming task in case it gets cancelled before we even start it
+ upcomingTasks.add(nodeId)
+
+ // get all incoming edges to this node
+ const incomers = (getConnectedEdges(nodeId) as ProcessEdge[]).filter((connection) => connection.target === nodeId)
+
+ // wait for edge animations to finish before starting the process
+ await Promise.all(incomers.map((incomer) => until(() => !incomer.data?.isAnimating)))
+
+ // remove the upcoming task since we are about to start it
+ upcomingTasks.clear()
+
+ if (!isRunning.value) {
+ // The process was stopped
+ return
+ }
+
+ // mark the node as executed, so it doesn't run again
+ executedNodes.add(nodeId)
+
+ updateNodeStatus(nodeId, ProcessStatus.RUNNING)
+
+ // simulate an async process with a random timeout between 1-2 seconds
+ const delay = Math.floor(Math.random() * 2000) + 1000
+
+ return new Promise((resolve) => {
+ const timeout = setTimeout(
+ async () => {
+ // get all children of this node
+ const children = graph.value.successors(nodeId) || []
+
+ // randomly decide whether the node will throw an error
+ const willThrowError = Math.random() < 0.15
+
+ // we avoid throwing an error on the starting node
+ if (!isStart && willThrowError) {
+ updateNodeStatus(nodeId, ProcessStatus.ERROR)
+
+ // if cancelOnError is true, we stop the process and mark all descendants as skipped
+ if (toValue(cancelOnError)) {
+ await skipDescendants(nodeId)
+ runningTasks.delete(nodeId)
+
+ resolve(true)
+ return
+ }
+ }
+
+ updateNodeStatus(nodeId, ProcessStatus.FINISHED)
+
+ runningTasks.delete(nodeId)
+
+ if (children.length > 0) {
+ // run the process on the children in parallel
+ await Promise.all(children.map((child) => runNode(child)))
+ }
+
+ resolve(true)
+ },
+ // if this is a starting node, we don't want to wait
+ isStart ? 0 : delay,
+ )
+
+ // save the timeout so we can cancel it if needed
+ runningTasks.set(nodeId, timeout)
+ })
+ }
+
+ /**
+ * Run a sequence of nodes.
+ * It will start with the nodes that have no predecessors and then run the process on each node in sequence.
+ * If a node has multiple descendants, it will run them in parallel.
+ * If an error occurs, it will stop the process and mark all descendants as skipped.
+ * If cancelOnError is true, it will stop the process if an error occurs.
+ * If the process is stopped, it will mark all running nodes as cancelled.
+ *
+ * @param nodes The nodes to run.
+ */
+ async function run(nodes: ProcessNode[]) {
+ // if the process is already running, we don't want to start it again
+ if (isRunning.value) {
+ return
+ }
+
+ // reset all nodes to their initial state
+ reset(nodes)
+
+ isRunning.value = true
+
+ // get all starting nodes (nodes with no predecessors)
+ const startingNodes = nodes.filter((node) => graph.value.predecessors(node.id)?.length === 0)
+
+ // run the process on all starting nodes in parallel
+ await Promise.all(startingNodes.map((node) => runNode(node.id, true)))
+
+ clear()
+ }
+
+ /**
+ * Reset all nodes to their initial state.
+ *
+ * @param nodes The nodes to reset.
+ */
+ function reset(nodes: ProcessNode[]) {
+ clear()
+
+ for (const node of nodes) {
+ updateNodeStatus(node.id, null)
+ }
+ }
+
+ /**
+ * Skip all descendants of a node.
+ *
+ * @param nodeId The ID of the node to skip descendants for.
+ */
+ async function skipDescendants(nodeId: string) {
+ const children = graph.value.successors(nodeId) || []
+
+ for (const child of children) {
+ updateNodeStatus(child, ProcessStatus.SKIPPED)
+ await skipDescendants(child)
+ }
+ }
+
+ /**
+ * Stop the process.
+ *
+ * It will mark all running nodes as cancelled and skip all upcoming tasks.
+ */
+ async function stop() {
+ isRunning.value = false
+
+ for (const nodeId of upcomingTasks) {
+ clearTimeout(runningTasks.get(nodeId))
+ runningTasks.delete(nodeId)
+ updateNodeStatus(nodeId, ProcessStatus.CANCELLED)
+ await skipDescendants(nodeId)
+ }
+
+ for (const [nodeId, task] of runningTasks) {
+ clearTimeout(task)
+ runningTasks.delete(nodeId)
+ updateNodeStatus(nodeId, ProcessStatus.CANCELLED)
+ await skipDescendants(nodeId)
+ }
+
+ executedNodes.clear()
+ upcomingTasks.clear()
+ }
+
+ /**
+ * Clear all running tasks and executed nodes.
+ */
+ function clear() {
+ isRunning.value = false
+ executedNodes.clear()
+ runningTasks.clear()
+ }
+
+ /**
+ * Update the status of a node.
+ *
+ * @param nodeId The ID of the node to update.
+ * @param status The new status of the node.
+ */
+ function updateNodeStatus(nodeId: string, status: ProcessData['status']) {
+ updateNodeData(nodeId, { status })
+ }
+
+ return { run, stop, reset, isRunning }
+}
+
+async function until(condition: () => boolean) {
+ return new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (condition()) {
+ clearInterval(interval)
+ resolve(true)
+ }
+ }, 100)
+ })
+}
diff --git a/examples/vite/src/Layouting/edges/ProcessEdge.vue b/examples/vite/src/Layouting/edges/ProcessEdge.vue
new file mode 100644
index 00000000..c4f1311b
--- /dev/null
+++ b/examples/vite/src/Layouting/edges/ProcessEdge.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+