metamaps--metamaps/app/assets/javascripts/src/Metamaps.JIT.js
Devin Howard 3b24165fd0 Merge pull request #589 from metamaps/feature/topicview-remove-topics
enable removal of topics from topic view - fixes #467
2016-08-08 21:46:15 +08:00

1923 lines
65 KiB
JavaScript

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 = Metamaps.Erb['topic_description_signifier.png']
self.topicLinkImage = new Image()
self.topicLinkImage.src = Metamaps.Erb['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)
if (Metamaps.Mappings) 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) {
Metamaps.Visualize.mGraph.events.touched = true
var canvas = Metamaps.Visualize.mGraph.canvas
Metamaps.Touch.touchPos = eventInfo.getPos()
Metamaps.Touch.touchPos.x *= canvas.scaleOffsetX
Metamaps.Touch.touchPos.y *= canvas.scaleOffsetY
Metamaps.Touch.touchPos.x += canvas.translateOffsetX
Metamaps.Touch.touchPos.y += canvas.translateOffsetY
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 = []
var yOffset = []
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 (Metamaps.tempInit == false) {
Metamaps.tempNode = node
Metamaps.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: Metamaps.tempNode.pos.getc().x,
y: Metamaps.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
Metamaps.tempNode2 = temp
Metamaps.Mouse.synapseEndCoordinates = {
x: Metamaps.tempNode2.pos.getc().x,
y: Metamaps.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) {
Metamaps.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) {
Metamaps.tempNode = null
if (Metamaps.tempNode2) Metamaps.tempNode2.setData('dim', 25, 'current')
Metamaps.tempNode2 = null
Metamaps.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 (Metamaps.tempInit && Metamaps.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 (Metamaps.tempInit && Metamaps.tempNode2 != null) {
// this means you want to create a synapse between two existing topics
Metamaps.Create.newTopic.addSynapse = false
Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id
Metamaps.Create.newSynapse.topic2id = Metamaps.tempNode2.getData('topic').id
Metamaps.tempNode2.setData('dim', 25, 'current')
Metamaps.Visualize.mGraph.plot()
midpoint.x = Metamaps.tempNode.pos.getc().x + (Metamaps.tempNode2.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2
midpoint.y = Metamaps.tempNode.pos.getc().y + (Metamaps.tempNode2.pos.getc().y - Metamaps.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()
Metamaps.tempNode = null
Metamaps.tempNode2 = null
Metamaps.tempInit = false
} else if (!Metamaps.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()
$('.rightclickmenu').remove()
// reset the draw synapse positions to false
Metamaps.Mouse.synapseStartCoordinates = []
Metamaps.Mouse.synapseEndCoordinates = null
Metamaps.tempInit = false
Metamaps.tempNode = null
Metamaps.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
getNodeXY: function(node) {
if (typeof node.pos.x === "number" && typeof node.pos.y === "number") {
return node.pos
} else if (typeof node.pos.theta === "number" && typeof node.pos.rho === "number") {
return new $jit.Polar(node.pos.theta, node.pos.rho).getc(true)
} else {
console.error('getNodeXY: unrecognized node pos format')
return {}
}
},
selectWithBox: function (e) {
var self = this
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 pos = self.getNodeXY(n)
var x = pos.x,
y = pos.y
// depending on which way the person dragged the box, check that
// x and y are between the start and end values of the box
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 fromNodePos = self.getNodeXY(edge.nodeFrom)
var fromNodeX = fromNodePos.x
var fromNodeY = -1 * fromNodePos.y
var toNodePos = self.getNodeXY(edge.nodeTo)
var toNodeX = toNodePos.x
var toNodeY = -1 * toNodePos.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 = '<ul>'
var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)
var disabled = authorized ? '' : 'disabled'
if (Metamaps.Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>'
if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>'
if (Metamaps.Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>'
if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>'
if (Metamaps.Active.Topic) {
menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Ctrl+E</div></li>'
}
menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>'
if (Metamaps.Active.Mapper) {
var options = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \
<li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \
<li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \
</ul>'
menustring += '<li class="rc-spacer"></li>'
menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + options + '<div class="expandLi"></div></li>'
var metacodeOptions = $('#metacodeOptions').html()
menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>'
}
if (Metamaps.Active.Topic) {
if (!Metamaps.Active.Mapper) {
menustring += '<li class="rc-spacer"></li>'
}
// set up the get sibling menu as a "lazy load"
// only fill in the submenu when they hover over the get siblings list item
var siblingMenu = '<ul id="fetchSiblingList"> \
<li class="fetchAll">All<div class="rc-keyboard">Ctrl+R</div></li> \
<li id="loadingSiblings"></li> \
</ul>'
menustring += '<li class="rc-siblings"><div class="rc-icon"></div>Reveal siblings' + siblingMenu + '<div class="expandLi"></div></li>'
}
menustring += '</ul>'
rightclickmenu.innerHTML = menustring
// position the menu where the click happened
var position = {}
var RIGHTCLICK_WIDTH = 300
var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static
var SUBMENUS_WIDTH = 256
var MAX_SUBMENU_HEIGHT = 270
var windowWidth = $(window).width()
var windowHeight = $(window).height()
if (windowWidth - e.clientX < SUBMENUS_WIDTH) {
position.right = windowWidth - e.clientX
$(rightclickmenu).addClass('moveMenusToLeft')
}
else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) {
position.right = windowWidth - e.clientX
}
else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) {
position.left = e.clientX
$(rightclickmenu).addClass('moveMenusToLeft')
}
else position.left = e.clientX
if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) {
position.bottom = windowHeight - e.clientY
$(rightclickmenu).addClass('moveMenusUp')
}
else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
position.top = e.clientY
$(rightclickmenu).addClass('moveMenusUp')
}
else position.top = e.clientY
$(rightclickmenu).css(position)
// add the menu to the page
$('#wrapper').append(rightclickmenu)
// attach events to clicks on the list items
// delete the selected things from the database
if (authorized) {
$('.rc-delete').click(function () {
$('.rightclickmenu').remove()
Metamaps.Control.deleteSelected()
})
}
// remove the selected things from the map
if (Metamaps.Active.Topic || authorized) {
$('.rc-remove').click(function () {
$('.rightclickmenu').remove()
Metamaps.Control.removeSelectedEdges()
Metamaps.Control.removeSelectedNodes()
})
}
// hide selected nodes and synapses until refresh
$('.rc-hide').click(function () {
$('.rightclickmenu').remove()
Metamaps.Control.hideSelectedEdges()
Metamaps.Control.hideSelectedNodes()
})
// when in radial, center on the topic you picked
$('.rc-center').click(function () {
$('.rightclickmenu').remove()
Metamaps.Topic.centerOn(node.id)
})
// open the entity in a new tab
$('.rc-popout').click(function () {
$('.rightclickmenu').remove()
var win = window.open('/topics/' + node.id, '_blank')
win.focus()
})
// change the permission of all the selected nodes and synapses that you were the originator of
$('.rc-permission li').click(function () {
$('.rightclickmenu').remove()
// $(this).text() will be 'commons' 'public' or 'private'
Metamaps.Control.updateSelectedPermissions($(this).text())
})
// change the metacode of all the selected nodes that you have edit permission for
$('.rc-metacode li li').click(function () {
$('.rightclickmenu').remove()
//
Metamaps.Control.updateSelectedMetacodes($(this).attr('data-id'))
})
// fetch relatives
var fetch_sent = false
$('.rc-siblings').hover(function () {
if (!fetch_sent) {
Metamaps.JIT.populateRightClickSiblings(node)
fetch_sent = true
}
})
$('.rc-siblings .fetchAll').click(function () {
$('.rightclickmenu').remove()
// data-id is a metacode id
Metamaps.Topic.fetchRelatives(node)
})
}, // selectNodeOnRightClickHandler,
populateRightClickSiblings: function (node) {
var self = Metamaps.JIT
// depending on how many topics are selected, do different things
var topic = node.getData('topic')
// add a loading icon for now
var loader = new CanvasLoader('loadingSiblings')
loader.setColor('#4FC059'); // default is '#000000'
loader.setDiameter(15) // default is 40
loader.setDensity(41) // default is 40
loader.setRange(0.9); // default is 1.3
loader.show() // Hidden by default
var topics = Metamaps.Topics.map(function (t) { return t.id })
var topics_string = topics.join()
var successCallback = function (data) {
$('#loadingSiblings').remove()
for (var key in data) {
var string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')'
$('#fetchSiblingList').append('<li class="getSiblings" data-id="' + key + '">' + string + '</li>')
}
$('.rc-siblings .getSiblings').click(function () {
$('.rightclickmenu').remove()
// data-id is a metacode id
Metamaps.Topic.fetchRelatives(node, $(this).attr('data-id'))
})
}
$.ajax({
type: 'GET',
url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topics_string,
success: successCallback,
error: function () {}
})
},
selectEdgeOnClickHandler: function (adj, 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.selectEdgeOnRightClickHandler(adj, e)
return
}
var check = self.nodeWasDoubleClicked()
if (check) {
self.edgeDoubleClickHandler(adj, e)
return
} else {
// wait a certain length of time, then check again, then run this code
setTimeout(function () {
if (!Metamaps.JIT.nodeWasDoubleClicked()) {
var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1
if (!e.shiftKey) {
Metamaps.Control.deselectAllNodes()
Metamaps.Control.deselectAllEdges()
}
if (edgeAlreadySelected) {
Metamaps.Control.deselectEdge(adj)
} else {
Metamaps.Control.selectEdge(adj)
}
Metamaps.Visualize.mGraph.plot()
}
}, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE)
}
}, // selectEdgeOnClickHandler
selectEdgeOnRightClickHandler: function (adj, e) {
// the 'node' variable is a JIT node, the one that was clicked on
// the 'e' variable is the click event
if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered
var authorized
e.preventDefault()
e.stopPropagation()
if (Metamaps.Visualize.mGraph.busy) return
Metamaps.Control.selectEdge(adj)
// 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 = '<ul>'
var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)
var disabled = authorized ? '' : 'disabled'
if (Metamaps.Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>'
if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>'
if (Metamaps.Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>'
if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>'
if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-spacer"></li>'
if (Metamaps.Active.Mapper) {
var permOptions = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \
<li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \
<li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \
</ul>'
menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>'
}
menustring += '</ul>'
rightclickmenu.innerHTML = menustring
// position the menu where the click happened
var position = {}
var RIGHTCLICK_WIDTH = 300
var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static
var SUBMENUS_WIDTH = 256
var MAX_SUBMENU_HEIGHT = 270
var windowWidth = $(window).width()
var windowHeight = $(window).height()
if (windowWidth - e.clientX < SUBMENUS_WIDTH) {
position.right = windowWidth - e.clientX
$(rightclickmenu).addClass('moveMenusToLeft')
}
else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) {
position.right = windowWidth - e.clientX
}
else position.left = e.clientX
if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) {
position.bottom = windowHeight - e.clientY
$(rightclickmenu).addClass('moveMenusUp')
}
else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) {
position.top = e.clientY
$(rightclickmenu).addClass('moveMenusUp')
}
else position.top = e.clientY
$(rightclickmenu).css(position)
// add the menu to the page
$('#wrapper').append(rightclickmenu)
// attach events to clicks on the list items
// delete the selected things from the database
if (authorized) {
$('.rc-delete').click(function () {
$('.rightclickmenu').remove()
Metamaps.Control.deleteSelected()
})
}
// remove the selected things from the map
if (authorized) {
$('.rc-remove').click(function () {
$('.rightclickmenu').remove()
Metamaps.Control.removeSelectedEdges()
Metamaps.Control.removeSelectedNodes()
})
}
// hide selected nodes and synapses until refresh
$('.rc-hide').click(function () {
$('.rightclickmenu').remove()
Metamaps.Control.hideSelectedEdges()
Metamaps.Control.hideSelectedNodes()
})
// change the permission of all the selected nodes and synapses that you were the originator of
$('.rc-permission li').click(function () {
$('.rightclickmenu').remove()
// $(this).text() will be 'commons' 'public' or 'private'
Metamaps.Control.updateSelectedPermissions($(this).text())
})
}, // selectEdgeOnRightClickHandler
SmoothPanning: function () {
var sx = Metamaps.Visualize.mGraph.canvas.scaleOffsetX,
sy = Metamaps.Visualize.mGraph.canvas.scaleOffsetY,
y_velocity = Metamaps.Mouse.changeInY, // initial y velocity
x_velocity = Metamaps.Mouse.changeInX, // initial x velocity
easing = 1 // frictional value
easing = 1
window.clearInterval(Metamaps.panningInt)
Metamaps.panningInt = setInterval(function () {
myTimer()
}, 1)
function myTimer () {
Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy)
$(document).trigger(Metamaps.JIT.events.pan)
easing = easing * 0.75
if (easing < 0.1) window.clearInterval(Metamaps.panningInt)
}
}, // SmoothPanning
renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) {
var ctx = canvas.getCtx()
// invert edge direction
if (swap) {
var tmp = from
from = to
to = tmp
}
// vect represents a line from tip to tail of the arrow
var vect = new $jit.Complex(to.x - from.x, to.y - from.y)
// scale it
vect.$scale(dim / vect.norm())
// compute the midpoint of the edge line
var newX = (to.x - from.x) * placement + from.x
var newY = (to.y - from.y) * placement + from.y
var midPoint = new $jit.Complex(newX, newY)
// move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint
var arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y)
// compute the tail intersection point with the edge line
var intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y)
// vector perpendicular to vect
var normal = new $jit.Complex(-vect.y / 2, vect.x / 2)
var v1 = intermediatePoint.add(normal)
var v2 = intermediatePoint.$add(normal.$scale(-1))
if (newSynapse) {
ctx.strokeStyle = '#4fc059'
ctx.lineWidth = 2
ctx.globalAlpha = 1
}
ctx.beginPath()
ctx.moveTo(from.x, from.y)
ctx.lineTo(to.x, to.y)
ctx.stroke()
ctx.beginPath()
ctx.moveTo(v1.x, v1.y)
ctx.lineTo(arrowPoint.x, arrowPoint.y)
ctx.lineTo(v2.x, v2.y)
ctx.stroke()
}, // renderMidArrow
renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) {
var self = Metamaps.JIT
var directionCat = synapse.get('category')
var direction = synapse.getDirection()
var pos = adj.nodeFrom.pos.getc(true)
var posChild = adj.nodeTo.pos.getc(true)
// plot arrow edge
if (!direction) {
// render nothing for this arrow if the direction couldn't be retrieved
} else if (directionCat == 'none') {
edgeHelper.line.render({
x: pos.x,
y: pos.y
}, {
x: posChild.x,
y: posChild.y
}, canvas)
} else if (directionCat == 'both') {
self.renderMidArrow({
x: pos.x,
y: pos.y
}, {
x: posChild.x,
y: posChild.y
}, 13, true, canvas, 0.7)
self.renderMidArrow({
x: pos.x,
y: pos.y
}, {
x: posChild.x,
y: posChild.y
}, 13, false, canvas, 0.7)
} else if (directionCat == 'from-to') {
var inv = (direction[0] != adj.nodeFrom.id)
self.renderMidArrow({
x: pos.x,
y: pos.y
}, {
x: posChild.x,
y: posChild.y
}, 13, inv, canvas, 0.7)
self.renderMidArrow({
x: pos.x,
y: pos.y
}, {
x: posChild.x,
y: posChild.y
}, 13, inv, canvas, 0.3)
}
}, // renderEdgeArrows
zoomIn: function (event) {
Metamaps.Visualize.mGraph.canvas.scale(1.25, 1.25)
$(document).trigger(Metamaps.JIT.events.zoom, [event])
},
zoomOut: function (event) {
Metamaps.Visualize.mGraph.canvas.scale(0.8, 0.8)
$(document).trigger(Metamaps.JIT.events.zoom, [event])
},
centerMap: function (canvas) {
var offsetScale = canvas.scaleOffsetX
canvas.scale(1 / offsetScale, 1 / offsetScale)
var offsetX = canvas.translateOffsetX
var offsetY = canvas.translateOffsetY
canvas.translate(-1 * offsetX, -1 * offsetY)
},
zoomToBox: function (event) {
var sX = Metamaps.Mouse.boxStartCoordinates.x,
sY = Metamaps.Mouse.boxStartCoordinates.y,
eX = Metamaps.Mouse.boxEndCoordinates.x,
eY = Metamaps.Mouse.boxEndCoordinates.y
var canvas = Metamaps.Visualize.mGraph.canvas
Metamaps.JIT.centerMap(canvas)
var height = $(document).height(),
width = $(document).width()
var spanX = Math.abs(sX - eX)
var spanY = Math.abs(sY - eY)
var ratioX = width / spanX
var ratioY = height / spanY
var newRatio = Math.min(ratioX, ratioY)
if (canvas.scaleOffsetX * newRatio <= 5 && canvas.scaleOffsetX * newRatio >= 0.2) {
canvas.scale(newRatio, newRatio)
}
else if (canvas.scaleOffsetX * newRatio > 5) {
newRatio = 5 / canvas.scaleOffsetX
canvas.scale(newRatio, newRatio)
} else {
newRatio = 0.2 / canvas.scaleOffsetX
canvas.scale(newRatio, newRatio)
}
var cogX = (sX + eX) / 2
var cogY = (sY + eY) / 2
canvas.translate(-1 * cogX, -1 * cogY)
$(document).trigger(Metamaps.JIT.events.zoom, [event])
Metamaps.Mouse.boxStartCoordinates = false
Metamaps.Mouse.boxEndCoordinates = false
Metamaps.Visualize.mGraph.plot()
},
zoomExtents: function (event, canvas, denySelected) {
Metamaps.JIT.centerMap(canvas)
var height = canvas.getSize().height,
width = canvas.getSize().width,
maxX, minX, maxY, minY, counter = 0
if (!denySelected && Metamaps.Selected.Nodes.length > 0) {
var nodes = Metamaps.Selected.Nodes
} else {
var nodes = _.values(Metamaps.Visualize.mGraph.graph.nodes)
}
if (nodes.length > 1) {
nodes.forEach(function (n) {
var x = n.pos.x,
y = n.pos.y
if (counter == 0 && n.getData('alpha') == 1) {
maxX = x
minX = x
maxY = y
minY = y
}
var arrayOfLabelLines = Metamaps.Util.splitLine(n.name, 30).split('\n'),
dim = n.getData('dim'),
ctx = 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
// only adjust these values if the node is not filtered
if (n.getData('alpha') == 1) {
maxX = Math.max(x + width / 2, maxX)
maxY = Math.max(y + n.getData('height') + 5 + height, maxY)
minX = Math.min(x - width / 2, minX)
minY = Math.min(y - dim, minY)
counter++
}
})
var spanX = maxX - minX
var spanY = maxY - minY
var ratioX = spanX / width
var ratioY = spanY / height
var cogX = (maxX + minX) / 2
var cogY = (maxY + minY) / 2
canvas.translate(-1 * cogX, -1 * cogY)
var newRatio = Math.max(ratioX, ratioY)
var scaleMultiplier = 1 / newRatio * 0.9
if (canvas.scaleOffsetX * scaleMultiplier <= 3 && canvas.scaleOffsetX * scaleMultiplier >= 0.2) {
canvas.scale(scaleMultiplier, scaleMultiplier)
}
else if (canvas.scaleOffsetX * scaleMultiplier > 3) {
scaleMultiplier = 3 / canvas.scaleOffsetX
canvas.scale(scaleMultiplier, scaleMultiplier)
} else {
scaleMultiplier = 0.2 / canvas.scaleOffsetX
canvas.scale(scaleMultiplier, scaleMultiplier)
}
$(document).trigger(Metamaps.JIT.events.zoom, [event])
}
else if (nodes.length == 1) {
nodes.forEach(function (n) {
var x = n.pos.x,
y = n.pos.y
canvas.translate(-1 * x, -1 * y)
$(document).trigger(Metamaps.JIT.events.zoom, [event])
})
}
}
}