escher-vis
Version:
Escher: A Web Application for Building, Sharing, and Embedding Data-Rich Visualizations of Biological Pathways
953 lines (884 loc) • 32.2 kB
JavaScript
/**
* Behavior. Defines the set of click and drag behaviors for the map, and keeps
* track of which behaviors are activated.
*
* A Behavior instance has the following attributes:
*
* my_behavior.rotation_drag, my_behavior.text_label_mousedown,
* my_behavior.text_label_click, my_behavior.selectable_mousedown,
* my_behavior.selectable_click, my_behavior.selectable_drag,
* my_behavior.node_mouseover, my_behavior.node_mouseout,
* my_behavior.label_mousedown, my_behavior.label_mouseover,
* my_behavior.label_mouseout, my_behavior.bezier_drag,
* my_behavior.bezier_mouseover, my_behavior.bezier_mouseout,
* my_behavior.reaction_label_drag, my_behavior.node_label_drag,
*
*/
var utils = require('./utils')
var build = require('./build')
var d3_drag = require('d3-drag').drag
var d3_select = require('d3-selection').select
var d3_mouse = require('d3-selection').mouse
var d3_selection = require('d3-selection')
var Behavior = utils.make_class()
// methods
Behavior.prototype = {
init: init,
toggle_rotation_mode: toggle_rotation_mode,
turn_everything_on: turn_everything_on,
turn_everything_off: turn_everything_off,
// toggle
toggle_selectable_click: toggle_selectable_click,
toggle_text_label_edit: toggle_text_label_edit,
toggle_selectable_drag: toggle_selectable_drag,
toggle_label_drag: toggle_label_drag,
toggle_label_mouseover: toggle_label_mouseover,
toggle_bezier_drag: toggle_bezier_drag,
// util
turn_off_drag: turn_off_drag,
// get drag behaviors
_get_selectable_drag: _get_selectable_drag,
_get_bezier_drag: _get_bezier_drag,
_get_reaction_label_drag: _get_reaction_label_drag,
_get_node_label_drag: _get_node_label_drag,
_get_generic_drag: _get_generic_drag,
_get_generic_angular_drag: _get_generic_angular_drag
}
module.exports = Behavior
// definitions
function init (map, undo_stack) {
this.map = map
this.undo_stack = undo_stack
// make an empty function that can be called as a behavior and does nothing
this.empty_behavior = function () {}
// rotation mode operates separately from the rest
this.rotation_mode_enabled = false
this.rotation_drag = d3_drag()
// behaviors to be applied
this.selectable_mousedown = null
this.text_label_mousedown = null
this.text_label_click = null
this.selectable_drag = this.empty_behavior
this.node_mouseover = null
this.node_mouseout = null
this.label_mousedown = null
this.label_mouseover = null
this.label_mouseout = null
this.bezier_drag = this.empty_behavior
this.bezier_mouseover = null
this.bezier_mouseout = null
this.reaction_label_drag = this.empty_behavior
this.node_label_drag = this.empty_behavior
this.dragging = false
this.turn_everything_on()
}
/**
* Toggle everything except rotation mode and text mode.
*/
function turn_everything_on () {
this.toggle_selectable_click(true)
this.toggle_selectable_drag(true)
this.toggle_label_drag(true)
this.toggle_label_mouseover(true)
}
/**
* Toggle everything except rotation mode and text mode.
*/
function turn_everything_off () {
this.toggle_selectable_click(false)
this.toggle_selectable_drag(false)
this.toggle_label_drag(false)
this.toggle_label_mouseover(false)
}
/**
* Listen for rotation, and rotate selected nodes.
*/
function toggle_rotation_mode (on_off) {
if (on_off === undefined) {
this.rotation_mode_enabled = !this.rotation_mode_enabled
} else {
this.rotation_mode_enabled = on_off
}
var selection_node = this.map.sel.selectAll('.node-circle')
var selection_background = this.map.sel.selectAll('#canvas')
if (this.rotation_mode_enabled) {
this.map.callback_manager.run('start_rotation')
var selected_nodes = this.map.get_selected_nodes()
if (Object.keys(selected_nodes).length === 0) {
console.warn('No selected nodes')
return
}
// show center
this.center = average_location(selected_nodes)
show_center.call(this)
// this.set_status('Drag to rotate.')
var map = this.map
var selected_node_ids = Object.keys(selected_nodes)
var reactions = this.map.reactions
var nodes = this.map.nodes
var beziers = this.map.beziers
var start_fn = function (d) {
// silence other listeners
d3_selection.event.sourceEvent.stopPropagation()
}
var drag_fn = function (d, angle, total_angle, center) {
var updated = build.rotate_nodes(selected_nodes, reactions,
beziers, angle, center)
map.draw_these_nodes(updated.node_ids)
map.draw_these_reactions(updated.reaction_ids)
}
var end_fn = function (d) {}
var undo_fn = function (d, total_angle, center) {
// undo
var these_nodes = {}
selected_node_ids.forEach(function (id) {
these_nodes[id] = nodes[id]
})
var updated = build.rotate_nodes(these_nodes, reactions,
beziers, -total_angle,
center)
map.draw_these_nodes(updated.node_ids)
map.draw_these_reactions(updated.reaction_ids)
}
var redo_fn = function (d, total_angle, center) {
// redo
var these_nodes = {}
selected_node_ids.forEach(function (id) {
these_nodes[id] = nodes[id]
})
var updated = build.rotate_nodes(these_nodes, reactions,
beziers, total_angle,
center)
map.draw_these_nodes(updated.node_ids)
map.draw_these_reactions(updated.reaction_ids) }
var center_fn = function () {
return this.center
}.bind(this)
this.rotation_drag = this._get_generic_angular_drag(start_fn, drag_fn,
end_fn, undo_fn,
redo_fn, center_fn,
this.map.sel)
selection_background.call(this.rotation_drag)
this.selectable_drag = this.rotation_drag
} else {
// turn off all listeners
hide_center.call(this)
selection_node.on('mousedown.center', null)
selection_background.on('mousedown.center', null)
selection_background.on('mousedown.drag', null)
selection_background.on('touchstart.drag', null)
this.rotation_drag = null
this.selectable_drag = null
}
// definitions
function show_center () {
var sel = this.map.sel.selectAll('#rotation-center').data([ 0 ])
var enter_sel = sel.enter().append('g').attr('id', 'rotation-center')
enter_sel.append('path').attr('d', 'M-32 0 L32 0')
.attr('class', 'rotation-center-line')
enter_sel.append('path').attr('d', 'M0 -32 L0 32')
.attr('class', 'rotation-center-line')
var update_sel = enter_sel.merge(sel)
update_sel.attr('transform',
'translate(' + this.center.x + ',' + this.center.y + ')')
.attr('visibility', 'visible')
.on('mouseover', function () {
var current = parseFloat(update_sel.selectAll('path').style('stroke-width'))
update_sel.selectAll('path').style('stroke-width', current * 2 + 'px')
})
.on('mouseout', function () {
update_sel.selectAll('path').style('stroke-width', null)
})
.call(d3_drag().on('drag', function () {
var cur = utils.d3_transform_catch(update_sel.attr('transform'))
var new_loc = [
d3_selection.event.dx + cur.translate[0],
d3_selection.event.dy + cur.translate[1]
]
update_sel.attr('transform', 'translate(' + new_loc + ')')
this.center = { x: new_loc[0], y: new_loc[1] }
}.bind(this)))
}
function hide_center(sel) {
this.map.sel.select('#rotation-center')
.attr('visibility', 'hidden')
}
function average_location(nodes) {
var xs = []
var ys = []
for (var node_id in nodes) {
var node = nodes[node_id]
if (node.x !== undefined)
xs.push(node.x)
if (node.y !== undefined)
ys.push(node.y)
}
return { x: utils.mean(xs),
y: utils.mean(ys) }
}
}
/**
* With no argument, toggle the node click on or off. Pass in a boolean argument
* to set the on/off state.
*/
function toggle_selectable_click (on_off) {
if (on_off === undefined) {
on_off = this.selectable_mousedown === null
}
if (on_off) {
var map = this.map
this.selectable_mousedown = function (d) {
// stop propogation for the buildinput to work right
d3_selection.event.stopPropagation()
// this.parentNode.__data__.was_selected = d3_select(this.parentNode).classed('selected')
// d3_select(this.parentNode).classed('selected', true)
}
this.selectable_click = function (d) {
// stop propogation for the buildinput to work right
d3_selection.event.stopPropagation()
// click suppressed. This DOES have en effect.
if (d3_selection.event.defaultPrevented) return
// turn off the temporary selection so select_selectable
// works. This is a bit of a hack.
// if (!this.parentNode.__data__.was_selected)
// d3_select(this.parentNode).classed('selected', false)
map.select_selectable(this, d, d3_selection.event.shiftKey)
// this.parentNode.__data__.was_selected = false
}
this.node_mouseover = function (d) {
d3_select(this).style('stroke-width', null)
var current = parseFloat(d3_select(this).style('stroke-width'))
if (!d3_select(this.parentNode).classed('selected'))
d3_select(this).style('stroke-width', current * 3 + 'px')
}
this.node_mouseout = function (d) {
d3_select(this).style('stroke-width', null)
}
} else {
this.selectable_mousedown = null
this.selectable_click = null
this.node_mouseover = null
this.node_mouseout = null
this.map.sel.select('#nodes')
.selectAll('.node-circle').style('stroke-width', null)
}
}
/**
* With no argument, toggle the text edit on mousedown on/off. Pass in a boolean
* argument to set the on/off state. The backup state is equal to
* selectable_mousedown.
*/
function toggle_text_label_edit (on_off) {
if (on_off === undefined) {
on_off = this.text_edit_mousedown == null
}
if (on_off) {
var map = this.map
var selection = this.selection
this.text_label_mousedown = function () {
if (d3_selection.event.defaultPrevented) {
return // mousedown suppressed
}
// run the callback
var coords_a = utils.d3_transform_catch(d3_select(this).attr('transform'))
.translate
var coords = { x: coords_a[0], y: coords_a[1] }
map.callback_manager.run('edit_text_label', null, d3_select(this), coords)
d3_selection.event.stopPropagation()
}
this.text_label_click = null
this.map.sel.select('#text-labels')
.selectAll('.label')
.classed('edit-text-cursor', true)
// add the new-label listener
this.map.sel.on('mousedown.new_text_label', function (node) {
// silence other listeners
d3_selection.event.preventDefault()
var coords = {
x: d3_mouse(node)[0],
y: d3_mouse(node)[1],
}
this.map.callback_manager.run('new_text_label', null, coords)
}.bind(this, this.map.sel.node()))
} else {
this.text_label_mousedown = this.selectable_mousedown
this.text_label_click = this.selectable_click
this.map.sel.select('#text-labels')
.selectAll('.label')
.classed('edit-text-cursor', false)
// remove the new-label listener
this.map.sel.on('mousedown.new_text_label', null)
this.map.callback_manager.run('hide_text_label_editor')
}
}
/**
* With no argument, toggle the node drag & bezier drag on or off. Pass in a
* boolean argument to set the on/off state.
*/
function toggle_selectable_drag (on_off) {
if (on_off === undefined) {
on_off = this.selectable_drag === this.empty_behavior
}
if (on_off) {
this.selectable_drag = this._get_selectable_drag(this.map, this.undo_stack)
this.bezier_drag = this._get_bezier_drag(this.map, this.undo_stack)
} else {
this.selectable_drag = this.empty_behavior
this.bezier_drag = this.empty_behavior
}
}
/**
* With no argument, toggle the label drag on or off. Pass in a boolean argument
* to set the on/off state.
* @param {Boolean} on_off - The new on/off state.
*/
function toggle_label_drag (on_off) {
if (on_off === undefined) {
on_off = this.label_drag === this.empty_behavior
}
if (on_off) {
this.reaction_label_drag = this._get_reaction_label_drag(this.map)
this.node_label_drag = this._get_node_label_drag(this.map)
} else {
this.reaction_label_drag = this.empty_behavior
this.node_label_drag = this.empty_behavior
}
}
/**
* With no argument, toggle the tooltips on mouseover labels.
* @param {Boolean} on_off - The new on/off state.
*/
function toggle_label_mouseover (on_off) {
if (on_off === undefined) {
on_off = this.label_mouseover === null
}
if (on_off) {
// Show/hide tooltip.
// @param {String} type - 'reaction_label' or 'node_label'
// @param {Object} d - D3 data for DOM element
this.label_mouseover = function (type, d) {
if (!this.dragging) {
this.map.callback_manager.run('show_tooltip', null, type, d)
}
}.bind(this)
this.label_mouseout = function () {
this.map.callback_manager.run('delay_hide_tooltip')
}.bind(this)
} else {
this.label_mouseover = null
}
}
/**
* With no argument, toggle the bezier drag on or off. Pass in a boolean
* argument to set the on/off state.
*/
function toggle_bezier_drag (on_off) {
if (on_off === undefined) {
on_off = this.bezier_drag === this.empty_behavior
}
if (on_off) {
this.bezier_drag = this._get_bezier_drag(this.map)
this.bezier_mouseover = function (d) {
d3_select(this).style('stroke-width', String(3)+'px')
}
this.bezier_mouseout = function (d) {
d3_select(this).style('stroke-width', String(1)+'px')
}
} else {
this.bezier_drag = this.empty_behavior
this.bezier_mouseover = null
this.bezier_mouseout = null
}
}
function turn_off_drag (sel) {
sel.on('mousedown.drag', null)
sel.on('touchstart.drag', null)
}
/**
* Drag the selected nodes and text labels.
* @param {} map -
* @param {} undo_stack -
*/
function _get_selectable_drag (map, undo_stack) {
// define some variables
var behavior = d3_drag()
var the_timeout = null
var total_displacement = null
// for nodes
var node_ids_to_drag = null
var reaction_ids = null
// for text labels
var text_label_ids_to_drag = null
var move_label = function (text_label_id, displacement) {
var text_label = map.text_labels[text_label_id]
text_label.x = text_label.x + displacement.x
text_label.y = text_label.y + displacement.y
}
var set_dragging = function (on_off) {
this.dragging = on_off
}.bind(this)
behavior.on('start', function (d) {
set_dragging(true)
// silence other listeners (e.g. nodes BELOW this one)
d3_selection.event.sourceEvent.stopPropagation()
// remember the total displacement for later
total_displacement = { x: 0, y: 0 }
// If a text label is selected, the rest is not necessary
if (d3_select(this).attr('class').indexOf('label') === -1) {
// Note that drag start is called even for a click event
var data = this.parentNode.__data__,
bigg_id = data.bigg_id,
node_group = this.parentNode
// Move element to back (for the next step to work). Wait 200ms
// before making the move, becuase otherwise the element will be
// deleted before the click event gets called, and selection
// will stop working.
the_timeout = setTimeout(function () {
node_group.parentNode.insertBefore(node_group,
node_group.parentNode.firstChild)
}, 200)
// prepare to combine metabolites
map.sel.selectAll('.metabolite-circle')
.on('mouseover.combine', function (d) {
if (d.bigg_id === bigg_id && d.node_id !== data.node_id) {
d3_select(this).style('stroke-width', String(12) + 'px')
.classed('node-to-combine', true)
}
})
.on('mouseout.combine', function (d) {
if (d.bigg_id === bigg_id) {
map.sel.selectAll('.node-to-combine')
.style('stroke-width', String(2) + 'px')
.classed('node-to-combine', false)
}
})
}
})
behavior.on('drag', function (d) {
// if this node is not already selected, then select this one and
// deselect all other nodes. Otherwise, leave the selection alone.
if (!d3_select(this.parentNode).classed('selected')) {
map.select_selectable(this, d)
}
// get the grabbed id
var grabbed = {}
if (d3_select(this).attr('class').indexOf('label') === -1) {
// if it is a node
grabbed['type'] = 'node'
grabbed['id'] = this.parentNode.__data__.node_id
} else {
// if it is a text label
grabbed['type'] = 'label'
grabbed['id'] = this.__data__.text_label_id
}
var selected_node_ids = map.get_selected_node_ids()
var selected_text_label_ids = map.get_selected_text_label_ids()
node_ids_to_drag = []; text_label_ids_to_drag = []
// choose the nodes and text labels to drag
if (grabbed['type']=='node' &&
selected_node_ids.indexOf(grabbed['id']) === -1) {
node_ids_to_drag.push(grabbed['id'])
} else if (grabbed['type'] === 'label' &&
selected_text_label_ids.indexOf(grabbed['id']) === -1) {
text_label_ids_to_drag.push(grabbed['id'])
} else {
node_ids_to_drag = selected_node_ids
text_label_ids_to_drag = selected_text_label_ids
}
reaction_ids = []
var displacement = {
x: d3_selection.event.dx,
y: d3_selection.event.dy,
}
total_displacement = utils.c_plus_c(total_displacement, displacement)
node_ids_to_drag.forEach(function (node_id) {
// update data
var node = map.nodes[node_id],
updated = build.move_node_and_dependents(node, node_id,
map.reactions,
map.beziers,
displacement)
reaction_ids = utils.unique_concat([ reaction_ids, updated.reaction_ids ])
// remember the displacements
// if (!(node_id in total_displacement)) total_displacement[node_id] = { x: 0, y: 0 }
// total_displacement[node_id] = utils.c_plus_c(total_displacement[node_id], displacement)
})
text_label_ids_to_drag.forEach(function (text_label_id) {
move_label(text_label_id, displacement)
// remember the displacements
// if (!(node_id in total_displacement)) total_displacement[node_id] = { x: 0, y: 0 }
// total_displacement[node_id] = utils.c_plus_c(total_displacement[node_id], displacement)
})
// draw
map.draw_these_nodes(node_ids_to_drag)
map.draw_these_reactions(reaction_ids)
map.draw_these_text_labels(text_label_ids_to_drag)
})
behavior.on('end', function () {
set_dragging(false)
if (node_ids_to_drag === null) {
// Drag end can be called when drag has not been called. In this, case, do
// nothing.
total_displacement = null
node_ids_to_drag = null
text_label_ids_to_drag = null
reaction_ids = null
the_timeout = null
return
}
// look for mets to combine
var node_to_combine_array = []
map.sel.selectAll('.node-to-combine').each(function (d) {
node_to_combine_array.push(d.node_id)
})
if (node_to_combine_array.length === 1) {
// If a node is ready for it, combine nodes
var fixed_node_id = node_to_combine_array[0],
dragged_node_id = this.parentNode.__data__.node_id,
saved_dragged_node = utils.clone(map.nodes[dragged_node_id]),
segment_objs_moved_to_combine = combine_nodes_and_draw(fixed_node_id,
dragged_node_id)
undo_stack.push(function () {
// undo
// put the old node back
map.nodes[dragged_node_id] = saved_dragged_node
var fixed_node = map.nodes[fixed_node_id],
updated_reactions = []
segment_objs_moved_to_combine.forEach(function (segment_obj) {
var segment = map.reactions[segment_obj.reaction_id].segments[segment_obj.segment_id]
if (segment.from_node_id==fixed_node_id) {
segment.from_node_id = dragged_node_id
} else if (segment.to_node_id==fixed_node_id) {
segment.to_node_id = dragged_node_id
} else {
console.error('Segment does not connect to fixed node')
}
// removed this segment_obj from the fixed node
fixed_node.connected_segments = fixed_node.connected_segments.filter(function (x) {
return !(x.reaction_id==segment_obj.reaction_id && x.segment_id==segment_obj.segment_id)
})
if (updated_reactions.indexOf(segment_obj.reaction_id)==-1)
updated_reactions.push(segment_obj.reaction_id)
})
map.draw_these_nodes([dragged_node_id])
map.draw_these_reactions(updated_reactions)
}, function () {
// redo
combine_nodes_and_draw(fixed_node_id, dragged_node_id)
})
} else {
// otherwise, drag node
// add to undo/redo stack
// remember the displacement, dragged nodes, and reactions
var saved_displacement = utils.clone(total_displacement),
// BUG TODO this variable disappears!
// Happens sometimes when you drag a node, then delete it, then undo twice
saved_node_ids = utils.clone(node_ids_to_drag),
saved_text_label_ids = utils.clone(text_label_ids_to_drag),
saved_reaction_ids = utils.clone(reaction_ids)
undo_stack.push(function () {
// undo
saved_node_ids.forEach(function (node_id) {
var node = map.nodes[node_id]
build.move_node_and_dependents(node, node_id, map.reactions,
map.beziers,
utils.c_times_scalar(saved_displacement, -1))
})
saved_text_label_ids.forEach(function (text_label_id) {
move_label(text_label_id,
utils.c_times_scalar(saved_displacement, -1))
})
map.draw_these_nodes(saved_node_ids)
map.draw_these_reactions(saved_reaction_ids)
map.draw_these_text_labels(saved_text_label_ids)
}, function () {
// redo
saved_node_ids.forEach(function (node_id) {
var node = map.nodes[node_id]
build.move_node_and_dependents(node, node_id, map.reactions,
map.beziers,
saved_displacement)
})
saved_text_label_ids.forEach(function (text_label_id) {
move_label(text_label_id, saved_displacement)
})
map.draw_these_nodes(saved_node_ids)
map.draw_these_reactions(saved_reaction_ids)
map.draw_these_text_labels(saved_text_label_ids)
})
}
// stop combining metabolites
map.sel.selectAll('.metabolite-circle')
.on('mouseover.combine', null)
.on('mouseout.combine', null)
// clear the timeout
clearTimeout(the_timeout)
// clear the shared variables
total_displacement = null
node_ids_to_drag = null
text_label_ids_to_drag = null
reaction_ids = null
the_timeout = null
})
return behavior
// definitions
function combine_nodes_and_draw (fixed_node_id, dragged_node_id) {
var dragged_node = map.nodes[dragged_node_id]
var fixed_node = map.nodes[fixed_node_id]
var updated_segment_objs = []
dragged_node.connected_segments.forEach(function (segment_obj) {
// change the segments to reflect
var segment
try {
segment = map.reactions[segment_obj.reaction_id].segments[segment_obj.segment_id]
if (segment === undefined) throw new Error('undefined segment')
} catch (e) {
console.warn('Could not find connected segment ' + segment_obj.segment_id)
return
}
if (segment.from_node_id==dragged_node_id) segment.from_node_id = fixed_node_id
else if (segment.to_node_id==dragged_node_id) segment.to_node_id = fixed_node_id
else {
console.error('Segment does not connect to dragged node')
return
}
// moved segment_obj to fixed_node
fixed_node.connected_segments.push(segment_obj)
updated_segment_objs.push(utils.clone(segment_obj))
return
})
// delete the old node
map.delete_node_data([dragged_node_id])
// turn off the class
map.sel.selectAll('.node-to-combine').classed('node-to-combine', false)
// draw
map.draw_everything()
// return for undo
return updated_segment_objs
}
}
function _get_bezier_drag (map) {
var move_bezier = function (reaction_id, segment_id, bez, bezier_id,
displacement) {
var segment = map.reactions[reaction_id].segments[segment_id]
segment[bez] = utils.c_plus_c(segment[bez], displacement)
map.beziers[bezier_id].x = segment[bez].x
map.beziers[bezier_id].y = segment[bez].y
}
var start_fn = function (d) {
d.dragging = true
}
var drag_fn = function (d, displacement, total_displacement) {
// draw
move_bezier(d.reaction_id, d.segment_id, d.bezier, d.bezier_id,
displacement)
map.draw_these_reactions([d.reaction_id], false)
map.draw_these_beziers([d.bezier_id])
}
var end_fn = function (d) {
d.dragging = false
}
var undo_fn = function (d, displacement) {
move_bezier(d.reaction_id, d.segment_id, d.bezier, d.bezier_id,
utils.c_times_scalar(displacement, -1))
map.draw_these_reactions([d.reaction_id], false)
map.draw_these_beziers([d.bezier_id])
}
var redo_fn = function (d, displacement) {
move_bezier(d.reaction_id, d.segment_id, d.bezier, d.bezier_id,
displacement)
map.draw_these_reactions([d.reaction_id], false)
map.draw_these_beziers([d.bezier_id])
}
return this._get_generic_drag(start_fn, drag_fn, end_fn, undo_fn, redo_fn,
this.map.sel)
}
function _get_reaction_label_drag (map) {
var move_label = function (reaction_id, displacement) {
var reaction = map.reactions[reaction_id]
reaction.label_x = reaction.label_x + displacement.x
reaction.label_y = reaction.label_y + displacement.y
}
var start_fn = function (d) {
// hide tooltips when drag starts
map.callback_manager.run('hide_tooltip')
}
var drag_fn = function (d, displacement, total_displacement) {
// draw
move_label(d.reaction_id, displacement)
map.draw_these_reactions([ d.reaction_id ])
}
var end_fn = function (d) {
}
var undo_fn = function (d, displacement) {
move_label(d.reaction_id, utils.c_times_scalar(displacement, -1))
map.draw_these_reactions([ d.reaction_id ])
}
var redo_fn = function (d, displacement) {
move_label(d.reaction_id, displacement)
map.draw_these_reactions([ d.reaction_id ])
}
return this._get_generic_drag(start_fn, drag_fn, end_fn, undo_fn, redo_fn,
this.map.sel)
}
function _get_node_label_drag (map) {
var move_label = function (node_id, displacement) {
var node = map.nodes[node_id]
node.label_x = node.label_x + displacement.x
node.label_y = node.label_y + displacement.y
}
var start_fn = function (d) {
// hide tooltips when drag starts
map.callback_manager.run('hide_tooltip')
}
var drag_fn = function (d, displacement, total_displacement) {
// draw
move_label(d.node_id, displacement)
map.draw_these_nodes([ d.node_id ])
}
var end_fn = function (d) {
}
var undo_fn = function (d, displacement) {
move_label(d.node_id, utils.c_times_scalar(displacement, -1))
map.draw_these_nodes ([ d.node_id ])
}
var redo_fn = function (d, displacement) {
move_label(d.node_id, displacement)
map.draw_these_nodes([ d.node_id ])
}
return this._get_generic_drag(start_fn, drag_fn, end_fn, undo_fn, redo_fn,
this.map.sel)
}
/**
* Make a generic drag behavior, with undo/redo.
*
* start_fn: function (d) Called at drag start.
*
* drag_fn: function (d, displacement, total_displacement) Called during drag.
*
* end_fn
*
* undo_fn
*
* redo_fn
*
* relative_to_selection: a d3 selection that the locations are calculated
* against.
*
*/
function _get_generic_drag (start_fn, drag_fn, end_fn, undo_fn, redo_fn,
relative_to_selection) {
// define some variables
var behavior = d3_drag()
var total_displacement
var undo_stack = this.undo_stack
var rel = relative_to_selection.node()
behavior.on('start', function (d) {
this.dragging = true
// silence other listeners
d3_selection.event.sourceEvent.stopPropagation()
total_displacement = { x: 0, y: 0 }
start_fn(d)
}.bind(this))
behavior.on('drag', function (d) {
// update data
var displacement = {
x: d3_selection.event.dx,
y: d3_selection.event.dy,
}
var location = {
x: d3_mouse(rel)[0],
y: d3_mouse(rel)[1],
}
// remember the displacement
total_displacement = utils.c_plus_c(total_displacement, displacement)
drag_fn(d, displacement, total_displacement, location)
}.bind(this))
behavior.on('end', function (d) {
this.dragging = false
// add to undo/redo stack
// remember the displacement, dragged nodes, and reactions
var saved_d = utils.clone(d)
var saved_displacement = utils.clone(total_displacement) // BUG TODO this variable disappears!
var saved_location = {
x: d3_mouse(rel)[0],
y: d3_mouse(rel)[1],
}
undo_stack.push(function () {
// undo
undo_fn(saved_d, saved_displacement, saved_location)
}, function () {
// redo
redo_fn(saved_d, saved_displacement, saved_location)
})
end_fn(d)
}.bind(this))
return behavior
}
/** Make a generic drag behavior, with undo/redo. Supplies angles in place of
* displacements.
*
* start_fn: function (d) Called at drag start.
*
* drag_fn: function (d, displacement, total_displacement) Called during drag.
*
* end_fn:
*
* undo_fn:
*
* redo_fn:
*
* get_center:
*
* relative_to_selection: a d3 selection that the locations are calculated
* against.
*
*/
function _get_generic_angular_drag (start_fn, drag_fn, end_fn, undo_fn, redo_fn,
get_center, relative_to_selection) {
// define some variables
var behavior = d3_drag()
var total_angle
var undo_stack = this.undo_stack
var rel = relative_to_selection.node()
behavior.on('start', function (d) {
this.dragging = true
// silence other listeners
d3_selection.event.sourceEvent.stopPropagation()
total_angle = 0
start_fn(d)
}.bind(this))
behavior.on('drag', function (d) {
// update data
var displacement = {
x: d3_selection.event.dx,
y: d3_selection.event.dy,
}
var location = {
x: d3_mouse(rel)[0],
y: d3_mouse(rel)[1],
}
var center = get_center()
var angle = utils.angle_for_event(displacement, location, center)
// remember the displacement
total_angle = total_angle + angle
drag_fn(d, angle, total_angle, center)
}.bind(this))
behavior.on('end', function (d) {
this.dragging = false
// add to undo/redo stack
// remember the displacement, dragged nodes, and reactions
var saved_d = utils.clone(d)
var saved_angle = total_angle
var saved_center = utils.clone(get_center())
undo_stack.push(function () {
// undo
undo_fn(saved_d, saved_angle, saved_center)
}, function () {
// redo
redo_fn(saved_d, saved_angle, saved_center)
})
end_fn(d)
}.bind(this))
return behavior
}