From 08d2cbb00dd27056b99a26c03850c001ac9f1ff5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 1 Feb 2017 17:47:45 +0000 Subject: [PATCH] running algo each time an edge is added --- frontend/src/ConvoAlgo/index.js | 201 +++++++++++++++++++++++++++++ frontend/src/Metamaps/Control.js | 2 - frontend/src/Metamaps/Engine.js | 114 ++++++---------- frontend/src/Metamaps/JIT.js | 1 - frontend/src/Metamaps/Visualize.js | 2 +- 5 files changed, 238 insertions(+), 82 deletions(-) create mode 100644 frontend/src/ConvoAlgo/index.js diff --git a/frontend/src/ConvoAlgo/index.js b/frontend/src/ConvoAlgo/index.js new file mode 100644 index 00000000..428c915b --- /dev/null +++ b/frontend/src/ConvoAlgo/index.js @@ -0,0 +1,201 @@ +// an array of synapses +// an array of topics + +// a focal node + +/* +step 1 +generate an object/array that represents the intended layout + + +step 2 +generate x,y coordinates for every topic in the layout object + +step 3 +set end states for every topic + +Step 4 +animate +*/ + +// synapses = [{ topic1_id: 4, topic2_id: 5, direction: 'from-to' }] + +export const generateLayoutObject = (topics, synapses, focalTopicId) => { + let layout = [] // will be the final output + const usedTopics = {} // will store the topics that have been placed into islands + let newRoot + let currentTopic + + const addParentsAndChildren = (topic) => { + if (!topic.id) return topic + + usedTopics[topic.id] = true + + topic.parents = [] + topic.children = [] + + let filteredParentIds = synapses.filter(synapse => { + return synapse.topic2_id === topic.id + && !usedTopics[synapse.topic1_id] + && synapse.category === 'from-to' + }).map(synapse => synapse.topic1_id) + + let filteredChildrenIds = synapses.filter(synapse => { + return synapse.topic1_id === topic.id + && !usedTopics[synapse.topic2_id] + && synapse.category === 'from-to' + + }).map(synapse => synapse.topic2_id) + + filteredParentIds.forEach(parentId => { + let parent = { + id: parentId + } + topic.parents.push(addParentsAndChildren(parent)) + }) + + filteredChildrenIds.forEach(childId => { + let child = { + id: childId + } + topic.children.push(addParentsAndChildren(child)) + }) + + return topic + } + + // start with the focal node, and build its island + currentTopic = topics.find(t => t.id === focalTopicId) + if (!currentTopic) { + console.log('you didnt pass a valid focalTopicId') + return layout + } + newRoot = { + id: currentTopic.id + } + layout.push(addParentsAndChildren(newRoot)) + + // + // go through the the topics again, and build the island for the first topic that isn't + // yet in the usedTopics object (in any island). recurse + topics.forEach(topic => { + if (topic && topic.id && !usedTopics[topic.id]) { + newRoot = { + id: topic.id + } + layout.push(addParentsAndChildren(newRoot)) + } + }) + return layout +} + + +export const generateObjectCoordinates = (layoutObject, focalTopicId, focalCoords) => { + const coords = {} + + const traverseIsland = (island, func, parent, child) => { + func(island, parent, child) + if (island.parents) { + island.parents.forEach(p => traverseIsland(p, func, null, island)) + } + if (island.children) { + island.children.forEach(c => traverseIsland(c, func, island, null)) + } + } + + const positionTopic = (topic, parent, child) => { + if (topic.id === focalTopicId) { + // set the focalCoord to be what it already was + coords[topic.id] = focalCoords + } else if (!parent && !child) { + coords[topic.id] = {x: 0, y: 150} + } else if (parent) { + coords[topic.id] = { + x: coords[parent.id].x + 250, + y: coords[parent.id].y + } + } else if (child) { + coords[topic.id] = { + x: coords[child.id].x - 250, + y: coords[child.id].y + } + } + } + + // lay all of them out as if there were no other ones + layoutObject.forEach(island => { + traverseIsland(island, positionTopic) + }) + + // calculate the bounds of each island + + // reposition the islands according to the bounds + return coords +} + +export const getLayoutForData = (topics, synapses, focalTopicId, focalCoords) => { + return generateObjectCoordinates(generateLayoutObject(topics, synapses, focalTopicId), focalTopicId, focalCoords) +} + + + +// if we've placed a node into an island, we need to NOT place it in any other islands +// Every node should only appear in one island + +// the pseudo-focal node + + +// the top level array represents islands +// every island has some sort of 'focal' node +/* +var example = [ + // the island that contains the focal node + { + id: 21, + parents: [ + { + id: 25, + parents: [] + }, + { + id: 25, + parents: [] + } + ], + children: [{ + id: 26, + children: [] + }] + }, + // all other islands should not contain children on the top level node + { + id: 21, + // parents may contain children + parents: [ + { + id: 100, + parents: [ + { + id: 101, + parents: [], + children: [ + { + id: 103, + children: [] + } + ] + } + ] + }, + { + id: 102, + parents: [] + } + ] + }, + { + id: 21, + parents: [] + }, +] +*/ \ No newline at end of file diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 77462447..4caa5ccc 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -23,7 +23,6 @@ const Control = { node.selected = true node.setData('dim', 30, 'current') Selected.Nodes.push(node) - Engine.setNodeSleeping(node.getData('body_id'), true) }, deselectAllNodes: function() { var l = Selected.Nodes.length @@ -40,7 +39,6 @@ const Control = { // remove the node Selected.Nodes.splice( Selected.Nodes.indexOf(node), 1) - Engine.setNodeSleeping(node.getData('body_id'), false) }, deleteSelected: function() { if (!Active.Map) return diff --git a/frontend/src/Metamaps/Engine.js b/frontend/src/Metamaps/Engine.js index 06518532..5ac2d079 100644 --- a/frontend/src/Metamaps/Engine.js +++ b/frontend/src/Metamaps/Engine.js @@ -1,7 +1,8 @@ -import Matter, { Vector, Sleeping, World, Constraint, Composite, Runner, Common, Body, Bodies, Events } from 'matter-js' +//import Matter, { Vector, Sleeping, World, Constraint, Composite, Runner, Common, Body, Bodies, Events } from 'matter-js' import { last, sortBy, values } from 'lodash' import $jit from '../patched/JIT' +import { getLayoutForData } from '../ConvoAlgo' import Active from './Active' import Create from './Create' @@ -11,58 +12,46 @@ import JIT from './JIT' import Visualize from './Visualize' const Engine = { - focusBody: null, - newNodeConstraint: null, - newNodeBody: Bodies.circle(Mouse.newNodeCoords.x, Mouse.newNodeCoords.y, 1), init: (serverData) => { - Engine.engine = Matter.Engine.create() - Events.on(Engine.engine, 'afterUpdate', Engine.callUpdate) - if (!serverData.ActiveMapper) Engine.engine.world.gravity.scale = 0 - else { - Engine.engine.world.gravity.y = 0 - Engine.engine.world.gravity.x = -1 - Body.setStatic(Engine.newNodeBody, true) - } + }, run: init => { if (init) { - if (Active.Mapper) World.addBody(Engine.engine.world, Engine.newNodeBody) - Visualize.mGraph.graph.eachNode(Engine.addNode) - DataModel.Synapses.each(s => Engine.addEdge(s.get('edge'))) if (Active.Mapper && Object.keys(Visualize.mGraph.graph.nodes).length) { Engine.setFocusNode(Engine.findFocusNode(Visualize.mGraph.graph.nodes)) + Engine.runLayout(true) } } - Engine.runner = Matter.Runner.run(Engine.engine) }, endActiveMap: () => { - Engine.runner && Runner.stop(Engine.runner) - Matter.Engine.clear(Engine.engine) + }, - setNodePos: (id, x, y) => { - const body = Composite.get(Engine.engine.world, id, 'body') - Body.setPosition(body, { x, y }) - Body.setVelocity(body, Vector.create(0, 0)) - Body.setAngularVelocity(body, 0) - Body.setAngle(body, 0) - }, - setNodeSleeping: (id, isSleeping) => { - const body = Composite.get(Engine.engine.world, id, 'body') - Sleeping.set(body, isSleeping) - if (!isSleeping) { - Body.setVelocity(body, Vector.create(0, 0)) - Body.setAngularVelocity(body, 0) - Body.setAngle(body, 0) - } + runLayout: init => { + const synapses = DataModel.Synapses.map(s => s.attributes) + const topics = DataModel.Topics.map(t => t.attributes) + const focalNodeId = Create.newSynapse.focusNode.getData('topic').id + const focalCoords = init ? { x: 0, y: 0 } : Create.newSynapse.focusNode.pos + const layout = getLayoutForData(topics, synapses, focalNodeId, focalCoords) + Visualize.mGraph.graph.eachNode(n => { + let calculatedCoords = layout[n.id] + if (!calculatedCoords) { + calculatedCoords = {x: 0, y: 0} + } + const endPos = new $jit.Complex(calculatedCoords.x, calculatedCoords.y) + n.setPos(endPos, 'end') + }) + Visualize.mGraph.animate({ + modes: ['linear'], + transition: $jit.Trans.Elastic.easeOut, + duration: 200, + onComplete: () => {} + }) }, addNode: node => { - let body = Bodies.circle(node.pos.x, node.pos.y, 100) - body.node_id = node.id - node.setData('body_id', body.id) - World.addBody(Engine.engine.world, body) + //Engine.runLayout() }, removeNode: node => { - + //Engine.runLayout() }, findFocusNode: nodes => { return last(sortBy(values(nodes), n => new Date(n.getData('topic').get('created_at')))) @@ -70,50 +59,19 @@ const Engine = { setFocusNode: node => { if (!Active.Mapper) return Create.newSynapse.focusNode = node - const body = Composite.get(Engine.engine.world, node.getData('body_id'), 'body') - Engine.focusBody = body - let constraint - if (Engine.newNodeConstraint) { - Engine.newNodeConstraint.bodyA = body - } - else { - constraint = Constraint.create({ - bodyA: body, - bodyB: Engine.newNodeBody, - length: JIT.ForceDirected.graphSettings.levelDistance, - stiffness: 0.2 - }) - World.addConstraint(Engine.engine.world, constraint) - Engine.newNodeConstraint = constraint + Mouse.focusNodeCoords = node.pos + Mouse.newNodeCoords = { + x: node.x + 200, + y: node.y } + Create.newSynapse.updateForm() + Create.newTopic.position() }, addEdge: edge => { - const bodyA = Composite.get(Engine.engine.world, edge.nodeFrom.getData('body_id'), 'body') - const bodyB = Composite.get(Engine.engine.world, edge.nodeTo.getData('body_id'), 'body') - let constraint = Constraint.create({ - bodyA, - bodyB, - length: JIT.ForceDirected.graphSettings.levelDistance, - stiffness: 0.2 - }) - edge.setData('constraint_id', constraint.id) - World.addConstraint(Engine.engine.world, constraint) + Engine.runLayout() }, - removeEdge: synapse => { - - }, - callUpdate: () => { - Engine.engine.world.bodies.forEach(b => { - const node = Visualize.mGraph.graph.getNode(b.node_id) - const newPos = new $jit.Complex(b.position.x, b.position.y) - node && node.setPos(newPos, 'current') - }) - if (Active.Mapper) { - if (Engine.focusBody) Mouse.focusNodeCoords = Engine.focusBody.position - Create.newSynapse.updateForm() - Create.newTopic.position() - } - Visualize.mGraph.plot() + removeEdge: edge => { + //Engine.runLayout() } } diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 0b21ca9d..0364f0a8 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1024,7 +1024,6 @@ const JIT = { n.pos.setp(theta, rho) } else { n.pos.setc(x, y) - Engine.setNodePos(n.getData('body_id'), x, y) } if (Active.Map) { diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 15247d36..d90e95f1 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -97,7 +97,7 @@ const Visualize = { }) //const startPos = new $jit.Complex(0, 0) - const endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) + const endPos = new $jit.Complex(0, 0) //n.setPos(startPos, 'start') //n.setPos(endPos, 'end') n.setPos(endPos, 'current')