Metamaps.JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', newTopic: 'Metamaps:JIT:events:newTopic', deleteTopic: 'Metamaps:JIT:events:deleteTopic', removeTopic: 'Metamaps:JIT:events:removeTopic', newSynapse: 'Metamaps:JIT:events:newSynapse', deleteSynapse: 'Metamaps:JIT:events:deleteSynapse', removeSynapse: 'Metamaps:JIT:events:removeSynapse', pan: 'Metamaps:JIT:events:pan', zoom: 'Metamaps:JIT:events:zoom', animationDone: 'Metamaps:JIT:events:animationDone', }, vizData: [], // contains the visualization-compatible graph /** * This method will bind the event handlers it is interested and initialize the class. */ init: function () { var self = Metamaps.JIT; $(".zoomIn").click(self.zoomIn); $(".zoomOut").click(self.zoomOut); var zoomExtents = function (event) { self.zoomExtents(event, Metamaps.Visualize.mGraph.canvas); }; $(".zoomExtents").click(zoomExtents); $(".takeScreenshot").click(Metamaps.Map.exportImage); self.topicDescImage = new Image(); self.topicDescImage.src = '<%= asset_path('topic_description_signifier.png') %>'; self.topicLinkImage = new Image(); self.topicLinkImage.src =<%= asset_path('topic_link_signifier.png') %>'; }, /** * convert our topic JSON into something JIT can use */ convertModelsToJIT: function(topics, synapses) { var jitReady = []; var synapsesToRemove = []; var topic; var mapping; var node; var nodes = {}; var existingEdge; var edge; var edges = []; topics.each(function (t) { node = t.createNode(); nodes[node.id] = node; }); synapses.each(function (s) { edge = s.createEdge(); if (topics.get(s.get('node1_id')) === undefined || topics.get(s.get('node2_id')) === undefined) { // this means it's an invalid synapse synapsesToRemove.push(s); } else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { existingEdge = _.findWhere(edges, { nodeFrom: edge.nodeFrom, nodeTo: edge.nodeTo }) || _.findWhere(edges, { nodeFrom: edge.nodeTo, nodeTo: edge.nodeFrom }); if (existingEdge) { // for when you're dealing with multiple relationships between the same two topics if (Metamaps.Active.Map) { mapping = s.getMapping(); existingEdge.data['$mappingIDs'].push(mapping.id); } existingEdge.data['$synapseIDs'].push(s.id); } else { // for when you're dealing with a topic that has relationships to many different nodes nodes[edge.nodeFrom].adjacencies.push(edge); edges.push(edge); } } }); _.each(nodes, function (node) { jitReady.push(node); }); return [jitReady, synapsesToRemove]; }, prepareVizData: function () { var self = Metamaps.JIT; var mapping; // reset/empty vizData self.vizData = []; Metamaps.Visualize.loadLater = false; var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses); self.vizData = results[0]; // clean up the synapses array in case of any faulty data _.each(results[1], function (synapse) { mapping = synapse.getMapping(); Metamaps.Synapses.remove(synapse); Metamaps.Mappings.remove(mapping); }); if (self.vizData.length == 0) { Metamaps.Famous.viz.showInstructions(); Metamaps.Visualize.loadLater = true; } else Metamaps.Famous.viz.hideInstructions(); Metamaps.Visualize.render(); }, // prepareVizData edgeRender: function (adj, canvas) { //get nodes cartesian coordinates var pos = adj.nodeFrom.pos.getc(true); var posChild = adj.nodeTo.pos.getc(true); var synapse; if(adj.getData("displayIndex")) { synapse = adj.getData("synapses")[adj.getData("displayIndex")]; if (!synapse) { delete adj.data.$displayIndex; synapse = adj.getData("synapses")[0]; } } else { synapse = adj.getData("synapses")[0]; } if (!synapse) return; // this means there are no corresponding synapses for // this edge, don't render it var directionCat = synapse.get("category"); //label placement on edges if (canvas.denySelected) { var color = Metamaps.Settings.colors.synapses.normal; canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color; } Metamaps.JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas); //check for edge label in data var desc = synapse.get("desc"); var showDesc = adj.getData("showDesc"); var drawSynapseCount = function (context, x, y, count) { /* circle size: 16x16px positioning: overlay and center on top right corner of synapse label - 8px left and 8px down color: #dab539 border color: #424242 border size: 1.5px font: DIN medium font-size: 14pt font-color: #424242 */ context.beginPath(); context.arc(x, y, 8, 0, 2 * Math.PI, false); context.fillStyle = '#DAB539'; context.strokeStyle = '#424242'; context.lineWidth = 1.5; context.closePath(); context.fill(); context.stroke(); // add the synapse count context.fillStyle = '#424242'; context.textAlign = 'center'; context.font = '14px din-medium'; context.fillText(count, x, y + 5); }; if (!canvas.denySelected && desc != "" && showDesc) { // '&' to '&' desc = Metamaps.Util.decodeEntities(desc); //now adjust the label placement var ctx = canvas.getCtx(); ctx.font = 'bold 14px arial'; ctx.fillStyle = '#FFF'; ctx.textBaseline = 'alphabetic'; var arrayOfLabelLines = Metamaps.Util.splitLine(desc, 30).split('\n'); var index, lineWidths = []; for (index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } var width = Math.max.apply(null, lineWidths) + 16; var height = (16 * arrayOfLabelLines.length) + 8; var x = (pos.x + posChild.x - width) / 2; var y = ((pos.y + posChild.y) / 2) - height / 2; var radius = 5; //render background ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); ctx.fill(); // get number of synapses var synapseNum = adj.getData("synapses").length; //render text ctx.fillStyle = '#424242'; ctx.textAlign = 'center'; for (index = 0; index < arrayOfLabelLines.length; ++index) { ctx.fillText(arrayOfLabelLines[index], x + (width / 2), y + 18 + (16 * index)); } if (synapseNum > 1) { drawSynapseCount(ctx, x + width, y, synapseNum); } } else if (!canvas.denySelected && showDesc) { // get number of synapses var synapseNum = adj.getData("synapses").length; if (synapseNum > 1) { var ctx = canvas.getCtx(); var x = (pos.x + posChild.x) / 2; var y = (pos.y + posChild.y) / 2; drawSynapseCount(ctx, x, y, synapseNum); } } }, // edgeRender ForceDirected: { animateSavedLayout: { modes: ['linear'], transition: $jit.Trans.Quad.easeInOut, duration: 800, onComplete: function () { Metamaps.Visualize.mGraph.busy = false; $(document).trigger(Metamaps.JIT.events.animationDone); } }, animateFDLayout: { modes: ['linear'], transition: $jit.Trans.Elastic.easeOut, duration: 800, onComplete: function () { Metamaps.Visualize.mGraph.busy = false; } }, graphSettings: { //id of the visualization container injectInto: 'infovis', //Enable zooming and panning //by scrolling and DnD Navigation: { enable: true, //Enable panning events only if we're dragging the empty //canvas (and not a node). panning: 'avoid nodes', zooming: 28 //zoom speed. higher is more sensible }, //background: { // type: 'Metamaps' //}, //NodeStyles: { // enable: true, // type: 'Native', // stylesHover: { // dim: 30 // }, // duration: 300 //}, // Change node and edge styles such as // color and width. // These properties are also set per node // with dollar prefixed data-properties in the // JSON structure. Node: { overridable: true, color: '#2D6A5D', type: 'customNode', dim: 25 }, Edge: { overridable: true, color: Metamaps.Settings.colors.synapses.normal, type: 'customEdge', lineWidth: 2, alpha: 1 }, //Native canvas text styling Label: { type: 'Native', //Native or HTML size: 20, family: 'arial', textBaseline: 'alphabetic', color: Metamaps.Settings.colors.labels.text }, //Add Tips Tips: { enable: false, onShow: function (tip, node) {} }, // Add node events Events: { enable: true, enableForEdges: true, onMouseMove: function (node, eventInfo, e) { Metamaps.JIT.onMouseMoveHandler(node, eventInfo, e); //console.log('called mouse move handler'); }, //Update node positions when dragged onDragMove: function (node, eventInfo, e) { Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e); //console.log('called drag move handler'); }, onDragEnd: function (node, eventInfo, e) { Metamaps.JIT.onDragEndTopicHandler(node, eventInfo, e, false); //console.log('called drag end handler'); }, onDragCancel: function (node, eventInfo, e) { Metamaps.JIT.onDragCancelHandler(node, eventInfo, e, false); }, //Implement the same handler for touchscreens onTouchStart: function (node, eventInfo, e) { //$jit.util.event.stop(e); //stop default touchmove event //Metamaps.Visualize.mGraph.events.onMouseDown(e, null, eventInfo); Metamaps.Visualize.mGraph.events.touched = true; Metamaps.Touch.touchPos = eventInfo.getPos(); var canvas = Metamaps.Visualize.mGraph.canvas, ox = canvas.translateOffsetX; oy = canvas.translateOffsetY, sx = canvas.scaleOffsetX, sy = canvas.scaleOffsetY; Metamaps.Touch.touchPos.x *= sx; Metamaps.Touch.touchPos.y *= sy; Metamaps.Touch.touchPos.x += ox; Metamaps.Touch.touchPos.y += oy; touchDragNode = node; }, //Implement the same handler for touchscreens onTouchMove: function (node, eventInfo, e) { if (Metamaps.Touch.touchDragNode) Metamaps.JIT.onDragMoveTopicHandler(Metamaps.Touch.touchDragNode, eventInfo, e); else { Metamaps.JIT.touchPanZoomHandler(eventInfo, e); } }, //Implement the same handler for touchscreens onTouchEnd: function (node, eventInfo, e) { }, //Implement the same handler for touchscreens onTouchCancel: function (node, eventInfo, e) { }, //Add also a click handler to nodes onClick: function (node, eventInfo, e) { // remove the rightclickmenu $('.rightclickmenu').remove(); if (Metamaps.Mouse.boxStartCoordinates) { if(e.ctrlKey){ Metamaps.Visualize.mGraph.busy = false; Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos(); var bS = Metamaps.Mouse.boxStartCoordinates; var bE = Metamaps.Mouse.boxEndCoordinates; if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { Metamaps.JIT.zoomToBox(e); return; } else { Metamaps.Mouse.boxStartCoordinates = null; Metamaps.Mouse.boxEndCoordinates = null; } //console.log('called zoom to box'); } if (e.shiftKey) { Metamaps.Visualize.mGraph.busy = false; Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos(); Metamaps.JIT.selectWithBox(e); //console.log('called select with box'); return; }; } if (e.target.id != "infovis-canvas") return false; //clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { Metamaps.JIT.selectEdgeOnClickHandler(node, e); //console.log('called selectEdgeOnClickHandler'); } else if (node && !node.nodeFrom) { Metamaps.JIT.selectNodeOnClickHandler(node, e); //console.log('called selectNodeOnClickHandler'); } else { Metamaps.JIT.canvasClickHandler(eventInfo.getPos(), e); //console.log('called canvasClickHandler'); } //if }, //Add also a click handler to nodes onRightClick: function (node, eventInfo, e) { // remove the rightclickmenu $('.rightclickmenu').remove(); if (Metamaps.Mouse.boxStartCoordinates) { Metamaps.Visualize.mGraph.busy = false; Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos(); Metamaps.JIT.selectWithBox(e); return; } if (e.target.id != "infovis-canvas") return false; //clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { Metamaps.JIT.selectEdgeOnRightClickHandler(node, e); } else if (node && !node.nodeFrom) { Metamaps.JIT.selectNodeOnRightClickHandler(node, e); } else { //console.log('right clicked on open space'); } } }, //Number of iterations for the FD algorithm iterations: 200, //Edge length levelDistance: 200, }, nodeSettings: { 'customNode': { 'render': function (node, canvas) { var pos = node.pos.getc(true), dim = node.getData('dim'), topic = node.getData('topic'), metacode = topic ? topic.getMetacode() : false, ctx = canvas.getCtx(); // if the topic is selected draw a circle around it if (!canvas.denySelected && node.selected) { ctx.beginPath(); ctx.arc(pos.x, pos.y, dim + 3, 0, 2 * Math.PI, false); ctx.strokeStyle = Metamaps.Settings.colors.topics.selected; ctx.lineWidth = 2; ctx.stroke(); } if (!metacode || !metacode.get('image') || !metacode.get('image').complete || (typeof metacode.get('image').naturalWidth !== "undefined" && metacode.get('image').naturalWidth === 0)) { ctx.beginPath(); ctx.arc(pos.x, pos.y, dim, 0, 2 * Math.PI, false); ctx.fillStyle = '#B6B2FD'; ctx.fill(); } else { ctx.drawImage(metacode.get('image'), pos.x - dim, pos.y - dim, dim * 2, dim * 2); } // if the topic has a link, draw a small image to indicate that var hasLink = topic && topic.get('link') !== "" && topic.get('link') !== null; var linkImage = Metamaps.JIT.topicLinkImage; var linkImageLoaded = linkImage.complete || (typeof linkImage.naturalWidth !== "undefined" && linkImage.naturalWidth !== 0) if (hasLink && linkImageLoaded) { ctx.drawImage(linkImage, pos.x - dim - 8, pos.y - dim - 8, 16, 16); } // if the topic has a desc, draw a small image to indicate that var hasDesc = topic && topic.get('desc') !== "" && topic.get('desc') !== null; var descImage = Metamaps.JIT.topicDescImage; var descImageLoaded = descImage.complete || (typeof descImage.naturalWidth !== "undefined" && descImage.naturalWidth !== 0) if (hasDesc && descImageLoaded) { ctx.drawImage(descImage, pos.x + dim - 8, pos.y - dim - 8, 16, 16); } }, 'contains': function (node, pos) { var npos = node.pos.getc(true), dim = node.getData('dim'), arrayOfLabelLines = Metamaps.Util.splitLine(node.name, 30).split('\n'), ctx = Metamaps.Visualize.mGraph.canvas.getCtx(); var height = 25 * arrayOfLabelLines.length; var index, lineWidths = []; for (index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } var width = Math.max.apply(null, lineWidths) + 8; var labely = npos.y + node.getData("height") + 5 + height / 2; var overLabel = this.nodeHelper.rectangle.contains({ x: npos.x, y: labely }, pos, width, height); return this.nodeHelper.circle.contains(npos, pos, dim) || overLabel; } } }, edgeSettings: { 'customEdge': { 'render': function (adj, canvas) { Metamaps.JIT.edgeRender(adj, canvas) }, 'contains': function (adj, pos) { var from = adj.nodeFrom.pos.getc(), to = adj.nodeTo.pos.getc(); // this fixes an issue where when edges are perfectly horizontal or perfectly vertical // it becomes incredibly difficult to hover over them if (-1 < pos.x && pos.x < 1) pos.x = 0; if (-1 < pos.y && pos.y < 1) pos.y = 0; return $jit.Graph.Plot.edgeHelper.line.contains(from, to, pos, adj.Edge.epsilon + 5); } } } }, // ForceDirected ForceDirected3D: { animate: { modes: ['linear'], transition: $jit.Trans.Elastic.easeOut, duration: 2500, onComplete: function () { Metamaps.Visualize.mGraph.busy = false; } }, graphSettings: { //id of the visualization container injectInto: 'infovis', type: '3D', Scene: { Lighting: { enable: false, ambient: [0.5, 0.5, 0.5], directional: { direction: { x: 1, y: 0, z: -1 }, color: [0.9, 0.9, 0.9] } } }, //Enable zooming and panning //by scrolling and DnD Navigation: { enable: false, //Enable panning events only if we're dragging the empty //canvas (and not a node). panning: 'avoid nodes', zooming: 10 //zoom speed. higher is more sensible }, // Change node and edge styles such as // color and width. // These properties are also set per node // with dollar prefixed data-properties in the // JSON structure. Node: { overridable: true, type: 'sphere', dim: 15, color: '#ffffff' }, Edge: { overridable: false, type: 'tube', color: '#111', lineWidth: 3 }, //Native canvas text styling Label: { type: 'HTML', //Native or HTML size: 10, style: 'bold' }, // Add node events Events: { enable: true, type: 'Native', i: 0, onMouseMove: function (node, eventInfo, e) { //if(this.i++ % 3) return; var pos = eventInfo.getPos(); Metamaps.Visualize.cameraPosition.x += (pos.x - Metamaps.Visualize.cameraPosition.x) * 0.5; Metamaps.Visualize.cameraPosition.y += (-pos.y - Metamaps.Visualize.cameraPosition.y) * 0.5; Metamaps.Visualize.mGraph.plot(); }, onMouseWheel: function (delta) { Metamaps.Visualize.cameraPosition.z += -delta * 20; Metamaps.Visualize.mGraph.plot(); }, onClick: function () {} }, //Number of iterations for the FD algorithm iterations: 200, //Edge length levelDistance: 100 }, nodeSettings: { }, edgeSettings: { } }, // ForceDirected3D RGraph: { animate: { modes: ['polar'], duration: 800, onComplete: function () { Metamaps.Visualize.mGraph.busy = false; } }, // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ background: { //type: 'Metamaps', levelDistance: 200, numberOfCircles: 4, CanvasStyles: { strokeStyle: '#333', lineWidth: 1.5 } }, levelDistance: 200 }, onMouseEnter: function (edge) { var filtered = edge.getData('alpha') === 0; // don't do anything if the edge is filtered // or if the canvas is animating if (filtered || Metamaps.Visualize.mGraph.busy) return; $('canvas').css('cursor', 'pointer'); var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge); //following if statement only executes if the edge being hovered over is not selected if (edgeIsSelected == -1) { edge.setData('showDesc', true, 'current'); } edge.setDataset('end', { lineWidth: 4 }); Metamaps.Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth'], duration: 100 }); Metamaps.Visualize.mGraph.plot(); }, // onMouseEnter onMouseLeave: function (edge) { if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered $('canvas').css('cursor', 'default'); var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge); //following if statement only executes if the edge being hovered over is not selected if (edgeIsSelected == -1) { edge.setData('showDesc', false, 'current'); } edge.setDataset('end', { lineWidth: 2 }); Metamaps.Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth'], duration: 100 }); Metamaps.Visualize.mGraph.plot(); }, // onMouseLeave onMouseMoveHandler: function (node, eventInfo, e) { var self = Metamaps.JIT; if (Metamaps.Visualize.mGraph.busy) return; var node = eventInfo.getNode(); var edge = eventInfo.getEdge(); //if we're on top of a node object, act like there aren't edges under it if (node != false) { if (Metamaps.Mouse.edgeHoveringOver) { self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver); } $('canvas').css('cursor', 'pointer'); return; } if (edge == false && Metamaps.Mouse.edgeHoveringOver != false) { //mouse not on an edge, but we were on an edge previously self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver); } else if (edge != false && Metamaps.Mouse.edgeHoveringOver == false) { //mouse is on an edge, but there isn't a stored edge self.onMouseEnter(edge); } else if (edge != false && Metamaps.Mouse.edgeHoveringOver != edge) { //mouse is on an edge, but a different edge is stored self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) self.onMouseEnter(edge); } //could be false Metamaps.Mouse.edgeHoveringOver = edge; if (!node && !edge) { $('canvas').css('cursor', 'default'); } }, // onMouseMoveHandler enterKeyHandler: function () { var creatingMap = Metamaps.GlobalUI.lightbox; if (creatingMap === "newmap" || creatingMap === "forkmap") { Metamaps.GlobalUI.CreateMap.submit(); } // this is to submit new topic creation else if (Metamaps.Create.newTopic.beingCreated) { Metamaps.Topic.createTopicLocally(); } // to submit new synapse creation else if (Metamaps.Create.newSynapse.beingCreated) { Metamaps.Synapse.createSynapseLocally(); } }, //enterKeyHandler escKeyHandler: function () { Metamaps.Control.deselectAllEdges(); Metamaps.Control.deselectAllNodes(); }, //escKeyHandler touchPanZoomHandler: function (eventInfo, e) { if (e.touches.length == 1) { var thispos = Metamaps.Touch.touchPos, currentPos = eventInfo.getPos(), canvas = Metamaps.Visualize.mGraph.canvas, ox = canvas.translateOffsetX, oy = canvas.translateOffsetY, sx = canvas.scaleOffsetX, sy = canvas.scaleOffsetY; currentPos.x *= sx; currentPos.y *= sy; currentPos.x += ox; currentPos.y += oy; //var x = currentPos.x - thispos.x, // y = currentPos.y - thispos.y; var x = currentPos.x - thispos.x, y = currentPos.y - thispos.y; Metamaps.Touch.touchPos = currentPos; Metamaps.Visualize.mGraph.canvas.translate(x * 1 / sx, y * 1 / sy); } else if (e.touches.length == 2) { var touch1 = e.touches[0]; var touch2 = e.touches[1]; var dist = Metamaps.Util.getDistance({ x: touch1.clientX, y: touch1.clientY }, { x: touch2.clientX, y: touch2.clientY }); if (!lastDist) { lastDist = dist; } var scale = dist / lastDist; if (8 >= Metamaps.Visualize.mGraph.canvas.scaleOffsetX * scale && Metamaps.Visualize.mGraph.canvas.scaleOffsetX * scale >= 1) { Metamaps.Visualize.mGraph.canvas.scale(scale, scale); } if (Metamaps.Visualize.mGraph.canvas.scaleOffsetX < 0.5) { Metamaps.Visualize.mGraph.canvas.viz.labels.hideLabels(true); } else if (Metamaps.Visualize.mGraph.canvas.scaleOffsetX > 0.5) { Metamaps.Visualize.mGraph.canvas.viz.labels.hideLabels(false); } lastDist = dist; } }, // touchPanZoomHandler onDragMoveTopicHandler: function (node, eventInfo, e) { var self = Metamaps.JIT; // this is used to send nodes that are moving to // other realtime collaborators on the same map var positionsToSend = {}; var topic; var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); if (node && !node.nodeFrom) { var pos = eventInfo.getPos(); // if it's a left click, or a touch, move the node if (e.touches || (e.button == 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons == 0 || e.buttons == 1 || e.buttons == undefined))) { //if the node dragged isn't already selected, select it var whatToDo = self.handleSelectionBeforeDragging(node, e); if (node.pos.rho || node.pos.rho === 0) { // this means we're in topic view var rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y); var theta = Math.atan2(pos.y, pos.x); node.pos.setp(theta, rho); } else if (whatToDo == 'only-drag-this-one') { node.pos.setc(pos.x, pos.y); if (Metamaps.Active.Map) { topic = node.getData('topic'); // we use the topic ID not the node id // because we can't depend on the node id // to be the same as on other collaborators // maps positionsToSend[topic.id] = pos; $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]); } } else { var len = Metamaps.Selected.Nodes.length; //first define offset for each node var xOffset = new Array(); var yOffset = new Array(); for (var i = 0; i < len; i += 1) { var n = Metamaps.Selected.Nodes[i]; xOffset[i] = n.pos.x - node.pos.x; yOffset[i] = n.pos.y - node.pos.y; } //for for (var i = 0; i < len; i += 1) { var n = Metamaps.Selected.Nodes[i]; var x = pos.x + xOffset[i]; var y = pos.y + yOffset[i]; n.pos.setc(x, y); if (Metamaps.Active.Map) { topic = n.getData('topic'); // we use the topic ID not the node id // because we can't depend on the node id // to be the same as on other collaborators // maps positionsToSend[topic.id] = n.pos; } } //for if (Metamaps.Active.Map) { $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]); } } //if if (whatToDo == 'deselect') { Metamaps.Control.deselectNode(node); } Metamaps.Visualize.mGraph.plot(); } // if it's a right click or holding down alt, start synapse creation ->third option is for firefox else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { if (tempInit == false) { tempNode = node; tempInit = true; Metamaps.Create.newTopic.hide(); Metamaps.Create.newSynapse.hide(); // set the draw synapse start positions var l = Metamaps.Selected.Nodes.length; if (l > 0) { for (var i = l - 1; i >= 0; i -= 1) { var n = Metamaps.Selected.Nodes[i]; Metamaps.Mouse.synapseStartCoordinates.push({ x: n.pos.getc().x, y: n.pos.getc().y }); } } else { Metamaps.Mouse.synapseStartCoordinates = [{ x: tempNode.pos.getc().x, y: tempNode.pos.getc().y }]; } Metamaps.Mouse.synapseEndCoordinates = { x: pos.x, y: pos.y }; } // temp = eventInfo.getNode(); if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned tempNode2 = temp; Metamaps.Mouse.synapseEndCoordinates = { x: tempNode2.pos.getc().x, y: tempNode2.pos.getc().y }; // before making the highlighted one bigger, make sure all the others are regular size Metamaps.Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current'); }); temp.setData('dim', 35, 'current'); Metamaps.Visualize.mGraph.plot(); } else if (!temp) { tempNode2 = null; Metamaps.Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current'); }); //pop up node creation :) var myX = e.clientX - 110; var myY = e.clientY - 30; $('#new_topic').css('left', myX + "px"); $('#new_topic').css('top', myY + "px"); Metamaps.Create.newTopic.x = eventInfo.getPos().x; Metamaps.Create.newTopic.y = eventInfo.getPos().y; Metamaps.Visualize.mGraph.plot(); Metamaps.Mouse.synapseEndCoordinates = { x: pos.x, y: pos.y }; } } else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Metamaps.Active.Topic) { Metamaps.GlobalUI.notifyUser("Cannot create in Topic view."); } else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); } } }, // onDragMoveTopicHandler onDragCancelHandler: function (node, eventInfo, e) { tempNode = null; if (tempNode2) tempNode2.setData('dim', 25, 'current'); tempNode2 = null; tempInit = false; // reset the draw synapse positions to false Metamaps.Mouse.synapseStartCoordinates = []; Metamaps.Mouse.synapseEndCoordinates = null; Metamaps.Visualize.mGraph.plot(); }, // onDragCancelHandler onDragEndTopicHandler: function (node, eventInfo, e) { var midpoint = {}, pixelPos, mapping; if (tempInit && tempNode2 == null) { // this means you want to add a new topic, and then a synapse Metamaps.Create.newTopic.addSynapse = true; Metamaps.Create.newTopic.open(); } else if (tempInit && tempNode2 != null) { // this means you want to create a synapse between two existing topics Metamaps.Create.newTopic.addSynapse = false; Metamaps.Create.newSynapse.topic1id = tempNode.getData('topic').id; Metamaps.Create.newSynapse.topic2id = tempNode2.getData('topic').id; tempNode2.setData('dim', 25, 'current'); Metamaps.Visualize.mGraph.plot(); midpoint.x = tempNode.pos.getc().x + (tempNode2.pos.getc().x - tempNode.pos.getc().x) / 2; midpoint.y = tempNode.pos.getc().y + (tempNode2.pos.getc().y - tempNode.pos.getc().y) / 2; pixelPos = Metamaps.Util.coordsToPixels(midpoint); $('#new_synapse').css('left', pixelPos.x + "px"); $('#new_synapse').css('top', pixelPos.y + "px"); Metamaps.Create.newSynapse.open(); tempNode = null; tempNode2 = null; tempInit = false; } else if (!tempInit && node && !node.nodeFrom) { // this means you dragged an existing node, autosave that to the database // check whether to save mappings var checkWhetherToSave = function() { var map = Metamaps.Active.Map; if (!map) return false; var mapper = Metamaps.Active.Mapper; // this case // covers when it is a public map owned by you // and also when it's a private map var activeMappersMap = map.authorizePermissionChange(mapper); var commonsMap = map.get('permission') === 'commons'; var realtimeOn = Metamaps.Realtime.status; // don't save if commons map, and you have realtime off, // even if you're map creator return map && mapper && ((commonsMap && realtimeOn) || (activeMappersMap && !commonsMap)); } if (checkWhetherToSave()) { mapping = node.getData('mapping'); mapping.save({ xloc: node.getPos().x, yloc: node.getPos().y }); // also save any other selected nodes that also got dragged along var l = Metamaps.Selected.Nodes.length; for (var i = l - 1; i >= 0; i -= 1) { var n = Metamaps.Selected.Nodes[i]; if (n !== node) { mapping = n.getData('mapping'); mapping.save({ xloc: n.getPos().x, yloc: n.getPos().y }); } }; } } }, //onDragEndTopicHandler canvasClickHandler: function (canvasLoc, e) { //grab the location and timestamp of the click var storedTime = Metamaps.Mouse.lastCanvasClick; var now = Date.now(); //not compatible with IE8 FYI Metamaps.Mouse.lastCanvasClick = now; var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper); if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE && !Metamaps.Mouse.didPan) { if (Metamaps.Active.Map && !authorized) { Metamaps.GlobalUI.notifyUser("Cannot edit Public map."); return; } else if (Metamaps.Active.Topic) { Metamaps.GlobalUI.notifyUser("Cannot create in Topic view."); return; } // DOUBLE CLICK //pop up node creation :) Metamaps.Create.newTopic.addSynapse = false; Metamaps.Create.newTopic.x = canvasLoc.x; Metamaps.Create.newTopic.y = canvasLoc.y; $('#new_topic').css('left', e.clientX + "px"); $('#new_topic').css('top', e.clientY + "px"); Metamaps.Create.newTopic.open(); } else if (!Metamaps.Mouse.didPan) { // SINGLE CLICK, no pan Metamaps.Filter.close(); Metamaps.TopicCard.hideCard(); Metamaps.SynapseCard.hideCard(); Metamaps.Create.newTopic.hide(); Metamaps.Create.newSynapse.hide(); $('.rightclickmenu').remove(); // reset the draw synapse positions to false Metamaps.Mouse.synapseStartCoordinates = []; Metamaps.Mouse.synapseEndCoordinates = null; tempInit = false; tempNode = null; tempNode2 = null; if (!e.ctrlKey && !e.shiftKey) { Metamaps.Control.deselectAllEdges(); Metamaps.Control.deselectAllNodes(); } } }, //canvasClickHandler nodeDoubleClickHandler: function (node, e) { Metamaps.TopicCard.showCard(node); }, // nodeDoubleClickHandler edgeDoubleClickHandler: function (adj, e) { Metamaps.SynapseCard.showCard(adj, e); }, // nodeDoubleClickHandler nodeWasDoubleClicked: function () { //grab the timestamp of the click var storedTime = Metamaps.Mouse.lastNodeClick; var now = Date.now(); //not compatible with IE8 FYI Metamaps.Mouse.lastNodeClick = now; if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) { return true; } else { return false; } }, //nodeWasDoubleClicked; handleSelectionBeforeDragging: function (node, e) { // four cases: // 1 nothing is selected, so pretend you aren't selecting // 2 others are selected only and shift, so additionally select this one // 3 others are selected only, no shift: drag only this one // 4 this node and others were selected, so drag them (just return false) //return value: deselect node again after? if (Metamaps.Selected.Nodes.length == 0) { return 'only-drag-this-one'; } if (Metamaps.Selected.Nodes.indexOf(node) == -1) { if (e.shiftKey) { Metamaps.Control.selectNode(node,e); return 'nothing'; } else { return 'only-drag-this-one'; } } return 'nothing'; //case 4? }, // handleSelectionBeforeDragging selectWithBox: function (e) { var sX = Metamaps.Mouse.boxStartCoordinates.x, sY = Metamaps.Mouse.boxStartCoordinates.y, eX = Metamaps.Mouse.boxEndCoordinates.x, eY = Metamaps.Mouse.boxEndCoordinates.y; if(!e.shiftKey){ Metamaps.Control.deselectAllNodes(); Metamaps.Control.deselectAllEdges(); } //select all nodes that are within the box Metamaps.Visualize.mGraph.graph.eachNode(function (n) { var x = n.pos.x, y = n.pos.y; if ((sX < x && x < eX && sY < y && y < eY) || (sX > x && x > eX && sY > y && y > eY) || (sX > x && x > eX && sY < y && y < eY) || (sX < x && x < eX && sY > y && y > eY)) { if(e.shiftKey){ if(n.selected){ Metamaps.Control.deselectNode(n); } else{ Metamaps.Control.selectNode(n,e); } } else{ Metamaps.Control.selectNode(n,e); } } }); //Convert selection box coordinates to traditional coordinates (+,+) in upper right sY = -1 * sY; eY = -1 * eY var edgesToToggle = []; Metamaps.Synapses.each(function(synapse) { var e = synapse.get('edge'); if (edgesToToggle.indexOf(e) === -1) { edgesToToggle.push(e); } }); edgesToToggle.forEach(function(edge) { var fromNodeX = edge.nodeFrom.pos.x; var fromNodeY = -1 * edge.nodeFrom.pos.y; var toNodeX = edge.nodeTo.pos.x; var toNodeY = -1 * edge.nodeTo.pos.y; var maxX = fromNodeX; var maxY = fromNodeY; var minX = fromNodeX; var minY = fromNodeY; //Correct maxX, MaxY values (toNodeX > maxX) ? (maxX = toNodeX):(minX = toNodeX); (toNodeY > maxY) ? (maxY = toNodeY):(minY = toNodeY); var maxBoxX = sX; var maxBoxY = sY; var minBoxX = sX; var minBoxY = sY; //Correct maxBoxX, maxBoxY values (eX > maxBoxX) ? (maxBoxX = eX):(minBoxX = eX); (eY > maxBoxY) ? (maxBoxY = eY):(minBoxY = eY); //Find the slopes from the synapse fromNode to the 4 corners of the selection box var slopes = []; slopes.push( (sY - fromNodeY) / (sX - fromNodeX) ); slopes.push( (sY - fromNodeY) / (eX - fromNodeX) ); slopes.push( (eY - fromNodeY) / (eX - fromNodeX) ); slopes.push( (eY - fromNodeY) / (sX - fromNodeX) ); var minSlope = slopes[0]; var maxSlope = slopes[0]; slopes.forEach(function(entry){ if(entry > maxSlope) maxSlope = entry; if(entry < minSlope) minSlope = entry; }); //Find synapse-in-question's slope var synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX); var b = fromNodeY - synSlope * fromNodeX; //Use the selection box edges as test cases for synapse intersection var testX = sX; var testY = synSlope * testX + b; var selectTest; if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY){ selectTest = true; } testX = eX; testY = synSlope * testX + b; if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY){ selectTest = true; } testY = sY; testX = (testY - b)/synSlope; if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testX >= minBoxX && testX <= maxBoxX){ selectTest = true; } testY = eY; testX = (testY - b)/synSlope; if(testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testX >= minBoxX && testX <= maxBoxX){ selectTest = true; } //Case where the synapse is wholly enclosed in the seldction box if(fromNodeX >= minBoxX && fromNodeX <= maxBoxX && fromNodeY >= minBoxY && fromNodeY <= maxBoxY && toNodeX >= minBoxX && toNodeX <= maxBoxX && toNodeY >= minBoxY && toNodeY <= maxBoxY){ selectTest = true; } //The test synapse was selected! if(selectTest){ // shiftKey = toggleSelect, otherwise if(e.shiftKey){ if(Metamaps.Selected.Edges.indexOf(edge) != -1 ){ Metamaps.Control.deselectEdge(edge); } else{ Metamaps.Control.selectEdge(edge); } } else{ Metamaps.Control.selectEdge(edge); } } }); Metamaps.Mouse.boxStartCoordinates = false; Metamaps.Mouse.boxEndCoordinates = false; Metamaps.Visualize.mGraph.plot(); }, // selectWithBox drawSelectBox: function (eventInfo, e) { var ctx = Metamaps.Visualize.mGraph.canvas.getCtx(); var startX = Metamaps.Mouse.boxStartCoordinates.x, startY = Metamaps.Mouse.boxStartCoordinates.y, currX = eventInfo.getPos().x, currY = eventInfo.getPos().y; Metamaps.Visualize.mGraph.canvas.clear(); Metamaps.Visualize.mGraph.plot(); ctx.beginPath(); ctx.moveTo(startX, startY); ctx.lineTo(startX, currY); ctx.lineTo(currX, currY); ctx.lineTo(currX, startY); ctx.lineTo(startX, startY); ctx.strokeStyle = "black"; ctx.stroke(); }, // drawSelectBox selectNodeOnClickHandler: function (node, e) { if (Metamaps.Visualize.mGraph.busy) return; var self = Metamaps.JIT; // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf("Mac") != -1 && e.ctrlKey) { self.selectNodeOnRightClickHandler(node, e) return; } // if on a topic page, let alt+click center you on a new topic if (Metamaps.Active.Topic && e.altKey) { Metamaps.RGraph.centerOn(node.id); return; } var check = self.nodeWasDoubleClicked(); if (check) { self.nodeDoubleClickHandler(node, e); return; } else { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!Metamaps.JIT.nodeWasDoubleClicked()) { var nodeAlreadySelected = node.selected; if (!e.shiftKey) { Metamaps.Control.deselectAllNodes(); Metamaps.Control.deselectAllEdges(); } if (nodeAlreadySelected) { Metamaps.Control.deselectNode(node); } else { Metamaps.Control.selectNode(node,e); } //trigger animation to final styles Metamaps.Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth:color:alpha'], duration: 500 }); Metamaps.Visualize.mGraph.plot(); } }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE); } }, //selectNodeOnClickHandler selectNodeOnRightClickHandler: function (node, e) { // the 'node' variable is a JIT node, the one that was clicked on // the 'e' variable is the click event e.preventDefault(); e.stopPropagation(); if (Metamaps.Visualize.mGraph.busy) return; // select the node Metamaps.Control.selectNode(node, e); // delete old right click menu $('.rightclickmenu').remove(); // create new menu for clicked on node var rightclickmenu = document.createElement("div"); rightclickmenu.className = "rightclickmenu"; // add the proper options to the menu var menustring = '