prevent node overlap on add new node - cytoscape.js

how do i prevent from overlapping new added nodes in cytoscape js graph?
i do not want change position of existing nodes
i want new adding nodes do automatically position by this rule that preventing existing node overlap
when i use add API to add nodes to graph added nodes overlap others
how can i change this behavior?
is there a solution for prevent node overlap in adding mode (new node do not overlap existing node)?
cy.add([
{ data: {label:"aaa" ,id:"bbb" } },
{ data: {label:"aaa333" ,id:"rrrr"} },
]);

There is no built in solution. I just look for an empty spot using this code for testing overlap (this is given a fixed node size, you could adapt it for using bounding boxes, but that slows things down).
function overlap(pos, size) {
var overlap = false;
cy.nodes(':visible').forEach(function (node) {
var npos = node.position();
if ((pos.x - size) < npos.x && (pos.x + size) > npos.x && (pos.y - size) < npos.y && (pos.y + size) > npos.y) {
overlap = true;
return false; //break
}
});
return overlap;
}
I use this with the prefered position as first argument ({x:..,y:..}), and when that fails I just add 10 to y to see if it fitst. Wether this is the best solution for you depends on the specific problem.

If you want an automated way of doing this, your best bet is a force-directed/physics layout.
Lock all but the new nodes when you run the layout.
You'll have to experiment with different layouts to see which particular one suits your app/data best.

Related

How to use selector :removed in cytoscape.js?

How can I get removed eles from the graph?
cy.filter(":removed") doesn't work and cy.elements() doesn't have removed eles.
Thanks!
Cytoscape.js does not keep removed elements in the graph. That would contradict that they're removed. So, of course if you ask the graph for all removed elements, it will return to you an empty collection -- i.e. there are no removed elements in the graph.
The :removed selector exists for completeness and so you can use it on your own collections. If you have a collection for which you've held onto a reference, some elements may be removed later on but they'll still be in the referenced collection.
You need to remember which nodes you removed and then call restore on them:
var removed = [];
function remove(elements)
{
removed.push(elements);
selected.remove();
}
function restore()
{
cy.startBatch();
for (var i = 0; i < removed.length; i++)
{
removed[i].restore();
}
removed = [];
cy.endBatch();
}
For some reason, removing takes a long time on a large graph. And if you only remove and restore nodes, their connected edges are gone.

Panning the map to certain extent javascript API

I want to limit map extent to the initial extent of the map and limit user from panning more than certain extent.
I tried following but nothing has changed:
map = new Map( "map" , {
basemap: "gray",
center: [-85.416, 49.000],
zoom : 6,
logo: false,
sliderStyle: "small"
});
dojo.connect(map, "onExtentChange", function (){
var initExtent = map.extent;
var extent = map.extent.getCenter();
if(initExtent.contains(extent)){}
else{map.setExtent(initExtent)}
});
Just to flesh out Simon's answer somewhat, and give an example. Ideally you need two variables at the same scope as map:
initExtent to store the boundary of your valid extent, and
validExtent to store the last valid extent found while panning, so that you can bounce back to it.
I've used the newer dojo.on event syntax as well for this example, it's probably a good idea to move to this as per the documentation's recommendation - I assume ESRI will discontinue the older style at some point.
var map;
var validExtent;
var initExtent;
[...]
require(['dojo/on'], function(on) {
on(map, 'pan', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
console.log('Outside bounds!');
} else {
console.log('Updated extent');
validExtent = evt.extent;
}
});
on(map, 'pan-end', function(evt) {
if ( !initExtent.contains(evt.extent) ) {
map.setExtent(validExtent);
}
});
});
You can do the same with the zoom events, or use extent-change if you want to trap everything. Up to you.
It looks like your extent changed function is setting the initial extent variable to the maps current extent and then checking if that extent contains the current extents centre point - which of course it always will.
Instead, declare initExtent at the same scope of the map variable. Then, change the on load event to set this global scope variable rather than a local variable. In the extent changed function, don't update the value of initExtent, simply check the initExtent contains the entire of the current extent.
Alternatively you could compare each bound of the current extent to each bound of the initExtent, e.g. is initExtent.xmin < map.extent.xmin and if any are, create a new extent setting any exceeded bounds to the initExtent values.
The only problem is these techniques will allow the initExtent to be exceeded briefly, but will then snap the extent back once the extent changed function fires and catches up.
I originally posted this solution on gis.stackexchange in answer to this question: https://gis.stackexchange.com/a/199366
Here's a code sample from that post:
//This function limits the extent of the map to prevent users from scrolling
//far away from the initial extent.
function limitMapExtent(map) {
var initialExtent = map.extent;
map.on('extent-change', function(event) {
//If the map has moved to the point where it's center is
//outside the initial boundaries, then move it back to the
//edge where it moved out
var currentCenter = map.extent.getCenter();
if (!initialExtent.contains(currentCenter) &&
event.delta.x !== 0 && event.delta.y !== 0) {
var newCenter = map.extent.getCenter();
//check each side of the initial extent and if the
//current center is outside that extent,
//set the new center to be on the edge that it went out on
if (currentCenter.x < initialExtent.xmin) {
newCenter.x = initialExtent.xmin;
}
if (currentCenter.x > initialExtent.xmax) {
newCenter.x = initialExtent.xmax;
}
if (currentCenter.y < initialExtent.ymin) {
newCenter.y = initialExtent.ymin;
}
if (currentCenter.y > initialExtent.ymax) {
newCenter.y = initialExtent.ymax;
}
map.centerAt(newCenter);
}
});
}
And here's a working jsFiddle example: http://jsfiddle.net/sirhcybe/aL1p24xy/

How to add an image to an item in a dojo/dnd/Source

I've create a dnd solution, with a source and target location. Right now the drag is bi-directional, I would like to have it be one-direction target to source. Then from the source add a image to each item (a delete icon), so that the users can then click the icon and send the record back to the correct target.
Part 1, I am trying to understand how to make the dnd one-directional and Part 2, how do I add an image to each item.
Thanks
There are events which handles this in the dnd module.
as for 'part1', see this http://dojotoolkit.org/reference-guide/1.8/dojo/dnd.html#id6 and set is'Source : false on the target which should only be a target.
and with 'part2' go to http://dojotoolkit.org/reference-guide/1.8/dojo/dnd.html#id8 and read about 'onDrop'. If overloading that function in your 'target-only' source, you will gain access to the nodes which is being dropped.
onDrop: function(source, nodes, copy) {
dojo.forEach(nodes, function(node) {
node.innerHTML += "<button title=Delete> X </button>";
});
}
This is what I got working.
dojo.connect(selectedInstructions, "onDndDrop", instructions.addDeleteButton);
addDeleteButton: function (source, nodes, copy, target) {
if (source != target) {
dojo.forEach(nodes, function(node) {
var instructionId = node.getAttribute("id");
var oImg = document.createElement("img");
oImg.setAttribute('src', 'images/delete.png');
oImg.setAttribute('alt', 'Remove');
oImg.setAttribute('class', 'remove_instruction');
oImg.setAttribute('onClick', "javascript:instructions.removeInstruction('" + instructionId + "')");
document.getElementById(instructionId).appendChild(oImg);
});
}
},
I then tried to get on to work, since connect is being depreciated, but I didn't appear to have much luck. I will have to come back to it at a later date, as I am on a time crunch right now to get this code out.
on(selectedInstructions, "onDrop", instructions.addDeleteButton);
aspect.after(selectedInstructions, "onDrop", instructions.addDeleteButton);
Wish the Dojo documentation was better. Thanks goodness for the community and it's support though.

Dojo DnD Move Node Programmatically

I would like to know if there is a way to move the node programmatically in dojo Dnd? The reason is I would like to revert the changes to the drag and drop when the web service call triggered a failed save on the database. Here is what I have so far.
In my code, the node Id seems to be unrecognized by the dojo.dnd.Container.DelItem. I cannot just use the selected item on the target because this is a asynchronous webservice function callback. So the user may be selecting another node on the container when this is called.
function OnMoveWSComplete(strResult) {
var resultObj = eval('(' + strResult + ')');
var sourceContainer = eval('(' + objResult.sourceContainerId + ')');
var targetContainer = eval('(' + objResult.targetContainerId + ')');
var targetNodes = targetContainer.getAllNodes();
for (var iNode = 0; iNode < targetNodes.length; iNode++) {
var currId = getLastIdFromStr(targetNodes[iNode].id);
if (currId == resultObj.Id) {
var targetN = targetNodes[iNode];
var Name = targetNodes[iNode].childNodes[0].nodeValue;
targetContainer.delItem(targetNodes[iNode].id);
var origData = { Id: resultObj.Id, Name: Name };
sourceContainer.insertNodes(true, origData);
break;
}
}
}
EDIT: Solution (Thanks Eugene Lazutkin) [2009/11/30]:
/**
* Move one node from one container to the other
*/
function moveNode(nodeId, sourceContainer, targetContainer) {
var node = dojo.byId(nodeId);
// Save the data
var saveData = sourceContainer.map[nodeId].data;
// Do the move
sourceContainer.parent.removeChild(node);
targetContainer.parent.appendChild(node);
// Sync the DOM object → map
sourceContainer.sync();
targetContainer.sync();
// Restore data for recreation of data
targetContainer.map[nodeId].data = saveData;
}
It looks like you assume that delItem removes physical nodes. Take a look at the documentation — probably you want to move nodes between containers instead of deleting them from the map. One simple way to do that just to move DOM nodes between containers, and call sync() on both containers.
Addition: Here is a super-simple pseudocode-like example:
function move(node, source, target){
// normalize node and/or node id
node = dojo.byId(node);
// move it physically from one parent to another
// (from target to source) adding to the end
target.parent.appenChild(node);
// now it is moved from source to target
// let's synchronize both dojo.dnd.Source's
source.sync();
target.sync();
}
Or something along these lines should work. The important pieces:
Move node from one parent to another using any DOM operations you deem appropriate. I used appendChild(), but you can use insertBefore(), or anything else.
Synchronize both sources involved after the move.
Obviously it works if both sources use nodes of the same type and structure. If not, you should do something more complex, e.g., move everything you need emulating a real DnD move by publishing topics described in the documentation.
I have this function which moves selected nodes by button click:
source.forInItems(dojo.hitch(this, function(item, id, map) {
if (dojo.hasClass(id, "dojoDndItemAnchor")) {
target.onDrop(source, [ dojo.byId(id) ], false);
dojo.removeClass(id, "dojoDndItemAnchor");
}
}));
onDrop() is an overridable method, which is called upon item drop, and by default calls to method onDropExternal(source, nodes, copy).
I am doing the same thing right now. I was able to solve by doing the following.
Set the dnd/Source autoSync property to true
<div data-dojo-type="dojo.dnd.Source" accept="widget" class="source" data-dojo-props="autoSync: true">
After dragging it looses the dndtype so I had to re-add it using client side code. Also I remove the dojoDndItemAnchor class after drop.
$(node).removeClass('dojoDndItemAnchor').attr('dndtype', 'widget');

Dojo Tree Refresh

Using Dojo 1.3, after adding a child (i.e. folder or item) to a tree, is there a way to have it reflected immediately via refresh or some other method?
From the official Dojo manual
Updating a Tree
People often ask:
how do I update a tree (adding or
deleting items?)
You can't update the
tree directly, but rather you need to
update the model. Usually the model is
connected to a data store and in that
case you need to update the data
store. Thus, you need to use a data
store that allows updates (through
it's official API), like
dojo.data.ItemFileWriteStore.
how do I refresh a Tree from the
store?
This isn't supported. The store
needs to notify the tree of any
changes to the data. Currently this is
really only supported (out of the box)
by dojo.data.ItemFileWriteStore, as
setting up a client-server dojo.data
source where the server notifies the
client whenever the data has changed
is quite complicated, and beyond the
scope of dojo, which is a client-only
solution.
Say, if your model has the query `{type:'continent'} - meaning any items with this property are top-level-items, then the following will model extension will monitor changes and refresh the tree's view
var dataStore = new ItemFileWriteStore( { ... });
new Tree({
store: dataStore,
model: new ForestModel({
onNewItem: function(item, parentInfo){
if(this.store.getValue(item, 'type') == 'continent'){
this._requeryTop();
}
this.inherited(arguments);
}
}
});
This should in turn call childrenChanged in the tree and update it every time a new item is added.
See model reference
As an addition, if the item added is not a toplevel item, an immediate update should be doable with this statement. parent is the treenode which has had an item added to its children.
tree._collapseNode(parent);
parent.state = 'UNCHECKED';
tree._expandNode(parent);
A more or less 'standard' refresh of the tree can be achieved by following. Reason for it not being added to base implementation, i think is because it will break the linkage with DnD features on a tree
dojo.declare("My.Tree", [dijit.Tree], {
// Close the store? (So that the store will do a new fetch()).
reloadStoreOnRefresh : true,
update: function() {
this.model.store.clearOnClose = this.reloadStoreOnRefresh;
this.model.store.close();
// Completely delete every node from the dijit.Tree
delete this._itemNodesMap;
this._itemNodesMap = {};
this.rootNode.state = "UNCHECKED";
delete this.model.root.children;
this.model.root.children = null;
// Destroy the widget
this.rootNode.destroyRecursive();
// Recreate the model, (with the model again)
this.model.constructor(this.model)
// Rebuild the tree
this.postMixInProperties();
this._load();
}
}
);
I've solved this WITHOUT needing the refresh.
_refreshNodeMapping: function (newNodeData) {
if(!this._itemNodesMap[newNodeData.identity]) return;
var nodeMapToRefresh = this._itemNodesMap[newNodeData.identity][0].item;
var domNode = this._itemNodesMap[newNodeData.identity][0].domNode;
//For every updated value, reset the old ones
for(var val in newNodeData)
{
nodeMapToRefresh[val] = newNodeData[val];
if(val == 'label')
{
domNode.innerHTML = newNodeData[val];
}
}
}