Sunday, May 12, 2013

How to avoid overlapping of shapes

While working on the project of creating diagram editor, one of the requirements was to avoid shape overlapping. Graphiti does not have this functionality out of the box, but it offers possibility how you can implement it by yourself. Today will see how to avoid overlapping of shapes (ContainerShapes) in a Diagram.

There are few points where you can forbid overlapping shapes in your diagram. One of the point is the moving feature. In the move feature you can specify when you would like to move the shape, and if you allow to move the shape, to which position you are allowed to move it. In order to achieve this, you need to create your own MoveFeature by extending the DefaultMoveShapeFeature. The two functions that you need to override are canMoveShape, and postMoveShape. In the canMoveShape function, beside constraining the moving based on a business object, we will see how to check if there is already an existing shape on that position. This is done by checking all the relative positions of all children shapes that are of type ContainerShapes (meaning that is shape representing a business object, the most outer shape of some figure). This is done with the following code:


@Override
public boolean canMoveShape(IMoveShapeContext context) {
 boolean canMove = super.canMoveShape(context);

 GraphicsAlgorithm thisGa = context.getShape().getGraphicsAlgorithm();

 EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
 if( targetBo instanceof BusinessObject){
  canMove = true;
 }

 //Check the relative position of all children of the target container
 for(Shape shape : context.getTargetContainer().getChildren()){
  if(shape instanceof ContainerShape && !shape.equals(context.getShape())){ //It is another object
   GraphicsAlgorithm ga = shape.getGraphicsAlgorithm();

   //With only two points we can define the area of the shape
   //e.g:  (x,y)
   //        +---------+
   //       |         |
   //        +---------+
   //              (x1, y1)
   int x  = ga.getX(); //left most x
   int x1 = ga.getX() + ga.getWidth(); //right most x
   int y  = ga.getY(); //left most y
   int y1 = ga.getY() + ga.getHeight(); //right most y

   //If the new position of the moving shape are within one of the children, do not allow moving
   if((context.getX()+thisGa.getWidth()) > x && context.getX() < x1 && (context.getY()+thisGa.getHeight()) > y && context.getY() < y1){
    canMove = false;
   }

  }
 }
 return canMove;
}

Now that we allowed moving of the shape, we an move the shape inside some other shape. Inside its parent the shape can still be half inside the borders, and half outside. But we want to avoid this too. In order to do that we need to override the postMoveShape function. Here we check whether the moved shape is withing the borders of its parent. If it is not we correct it. This is done with the following code:

/**
* Post move correction
*/
@Override
protected void postMoveShape(IMoveShapeContext context) {
 EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
 EObject sourceBo = context.getSourceContainer().getLink().getBusinessObjects().get(0);



 Shape containerShape = context.getShape();
 GraphicsAlgorithm containerGa = containerShape.getGraphicsAlgorithm();

 ContainerShape parentContainer = (ContainerShape) containerShape.eContainer();
 GraphicsAlgorithm parentGa = parentContainer.getGraphicsAlgorithm();


 if(!parentGa.getGraphicsAlgorithmChildren().isEmpty()){
  parentGa = parentGa.getGraphicsAlgorithmChildren().get(0); //If there is a visible rectangle, get the visible then
 }

 //Make sure that the shape is not outside of the the parent visible rectangle
 if(containerGa.getX() <= parentGa.getX()+10){
  containerGa.setX(parentGa.getX()+10);
 }
 if(containerGa.getY() <= parentGa.getY()+30){
  containerGa.setY(parentGa.getY()+30);
 }

 if((containerGa.getX()+containerGa.getWidth()) > (parentGa.getX()+parentGa.getWidth())){
  int x = (parentGa.getX()+parentGa.getWidth())-containerGa.getWidth()-10;
  containerGa.setX(x);
 }

 if((containerGa.getY()+containerGa.getHeight()) > (parentGa.getY()+parentGa.getHeight())){
  int y = (parentGa.getY()+parentGa.getHeight()) - containerGa.getHeight() - 10;
  containerGa.setY(y);
 }
}

You can also restrict negative coordinates of a shape by overriding avoidNegativeCoordinates from the DeafaultMoveShapeFeature.
The final MoveFeature looks like this:

import org.eclipse.emf.ecore.EObject;
import org.eclipse.graphiti.features.IFeatureProvider;
import org.eclipse.graphiti.features.context.IMoveShapeContext;
import org.eclipse.graphiti.features.impl.DefaultMoveShapeFeature;
import org.eclipse.graphiti.mm.algorithms.GraphicsAlgorithm;
import org.eclipse.graphiti.mm.pictograms.ContainerShape;
import org.eclipse.graphiti.mm.pictograms.Shape;


public class MoveFeature extends DefaultMoveShapeFeature{

 public MoveFeature(IFeatureProvider fp) {
  super(fp);
 }

 protected boolean avoidNegativeCoordinates() {
  return true;
 }

 @Override
 public boolean canMoveShape(IMoveShapeContext context) {
  boolean canMove = super.canMoveShape(context);

  GraphicsAlgorithm thisGa = context.getShape().getGraphicsAlgorithm();

  EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
  if( targetBo instanceof BusinessObject){
   canMove = true;
  }

  //Check the relative position of all children of the target container
  for(Shape shape : context.getTargetContainer().getChildren()){
   if(shape instanceof ContainerShape && !shape.equals(context.getShape())){ //It is another object
    GraphicsAlgorithm ga = shape.getGraphicsAlgorithm();

    //With only two points we can define the area of the shape
    //e.g:  (x,y)
    //        +---------+
    //        |         |
    //        +---------+
    //              (x1, y1)
    int x  = ga.getX(); //left most x
    int x1 = ga.getX() + ga.getWidth(); //right most x
    int y  = ga.getY(); //left most y
    int y1 = ga.getY() + ga.getHeight(); //right most y

    //If the new position of the moving shape are within one of the children, do not allow moving
    if((context.getX()+thisGa.getWidth()) > x && context.getX() < x1 && (context.getY()+thisGa.getHeight()) > y && context.getY() < y1){
     canMove = false;
    }

   }
  }
  return canMove;
 }


 /**
  * Post move correction
  */
 @Override
 protected void postMoveShape(IMoveShapeContext context) {
  EObject targetBo = context.getTargetContainer().getLink().getBusinessObjects().get(0);
  EObject sourceBo = context.getSourceContainer().getLink().getBusinessObjects().get(0);
  
  
  Shape containerShape = context.getShape();
  GraphicsAlgorithm containerGa = containerShape.getGraphicsAlgorithm();

  ContainerShape parentContainer = (ContainerShape) containerShape.eContainer();
  GraphicsAlgorithm parentGa = parentContainer.getGraphicsAlgorithm();

  
  if(!parentGa.getGraphicsAlgorithmChildren().isEmpty()){
   parentGa = parentGa.getGraphicsAlgorithmChildren().get(0); //If there is a visible rectangle, get the visible then
  }

  //Make sure that the shape is not outside of the the parent visible rectangle
  if(containerGa.getX() <= parentGa.getX()+10){
   containerGa.setX(parentGa.getX()+10);
  }
  if(containerGa.getY() <= parentGa.getY()+30){
   containerGa.setY(parentGa.getY()+30);
  }

  if((containerGa.getX()+containerGa.getWidth()) > (parentGa.getX()+parentGa.getWidth())){
   int x = (parentGa.getX()+parentGa.getWidth())-containerGa.getWidth()-10;
   containerGa.setX(x);
  }

  if((containerGa.getY()+containerGa.getHeight()) > (parentGa.getY()+parentGa.getHeight())){
   int y = (parentGa.getY()+parentGa.getHeight()) - containerGa.getHeight() - 10;
   containerGa.setY(y);
  }
 }
}

Other point where you can avoid overlapping of shapes is when you add new feature to your Diagram. In the CreateFeature, canCreate function, you can apply the same code from your canMoveShape function and this way you can forbid creating shape if there is not enough space on your diagram on the place you want to add your feature.
Here you should know, that this will prevent you from creating this feature at all, if there is not enough space on your Diagram, so be careful with this restriction here.

Finally lets see how this looks like in action. From the picture you can't see the forbidden icon, but your diagram won't allow you to do this:
Figure 1: Overlapping of shapes
Also with this code you won't face this situation where half of your shape is inside, and half is outside your parent shape:
Figure 2: Overlapping of shapes 2


If you have further questions I will be happy to help you.

4 comments:

  1. The problem is that Graphiti treats group movements as a sequence of independent movements. Suppose you have shapes A - B , side by side. YOu want to move both together, to the right, so that A will be where in the previous B position. This should be allowed, but -unless I'm mistaken- your approach would forbid it.

    ReplyDelete
  2. Yes you are right. I never thought of this, but now when I tried it, it does not allow to make the move, unless you clear the area that shape B is taking. Well this certainly is a problem for some approaches. However, my requirement was only to avoid overlapping of shapes.

    One approach that I can think of right now is to remove the checking in the canMove() function, and place it in the postMoveShape() function, and from there re-position the shape near the one that is under it, to avoid the overlapping. This need some changes, I will try to do them soon and make another post.

    Thank you for you feedback!

    ReplyDelete
  3. Regarding "postMoveShape()", remember that, again, that's called after the individual shape movement, but before that the other shapes have been moved. This difficulty of knowing what is happening (move, resize) "globally", is a pain in Graphiti.

    ReplyDelete
  4. Yes I know, that is what I was referring to. You can move the shape on top of other, but the postMoveShape() will re-position it, to new location. However this opens up new issue, of not having space to move it, if the shapes are dense.

    There is preMoveShape() that can be overridden in the DefaultMoveShapeFeature. Maybe we can some take advantage there.

    You are right, Graphiti is a pain in the ass for this. Right now I am working on auto lay-outing algorithms (Some basics) and it is killing me. The native GEF lay-outing algorithms are not good if you have shapes inside shapes.
    Let me know if you come up with something.

    ReplyDelete