Auto add start & end points to links?

Our web app can work with either our new cloud micro services and also our legacy services. With the cloud the start, end, and all midpoints are persisted. With the legacy code it only persists midpoints because that’s what the Java UI uses and what is in existing customer data. When the new React app is connected to the legacy backend and reads diagram data, GoJS will automatically create the start and end points when the link is straight (no points at all in the data coming from server). If the link has a single bend then the server will return one point and GoJS will not automatically create the start and end points.

Is there a setting to make GoJS automatically create the start and end points for all links, not just the ones that have no points? Since our app is aware if it’s connected to legacy services it could set a prop to tell GoJS to automatically create start and end points.

If there isn’t a setting any suggestions?

For such links the easy solution is to call Link.invalidateRoute, which will recompute the standard route for that link given its property values.

If you want to maintain the intermediate points, this is a lot more work. At each end it depends on whether the port has a Spot that causes there to be an " end segment", which would require two extra points to the route (at that end), not just one point.

For each end of the route you can compute the ultimate point by calling https://gojs.net/latest/api/symbols/Link.html#getLinkPoint

So I’m close but the start and end points go to N, S, E, & W instead of towards the center of the node.

This is how it should look.

Here is my code

  updateLinks = (event, diagram) => {
    const {
      formik: { resetForm, setFieldValue, values }
    } = this.props;
    const {
      diagramContainerContext: { insertLinkStartEndPoints }
    } = this;
    const { transitions = [] } = values;
    const { model } = event;
    const changes = JSON.parse(model.toIncrementalJson(event));
    const modified = changes.modifiedLinkData || [];
    const inserted = changes.insertedLinkKeys || [];
    const removed = changes.removedLinkKeys || [];
    const updatedTransitions = [...transitions];
    const bindings = [];
    let newTranList;

    if (event.oldValue === 'Initial Layout' && insertLinkStartEndPoints) {
      transitions.forEach(transition => {
        const newPoints = new go.List();
        const link = diagram.findLinkForData(model.findLinkDataForKey(transition.id));
        const fromNode = diagram.findNodeForKey(transition.fromNodeId);
        const toNode = diagram.findNodeForKey(transition.toNodeId);

        newPoints.add(link.getLinkPoint(fromNode, fromNode.port, go.Spot.Center, true, true, toNode, toNode.port));
        transition.points.forEach(point => {
          newPoints.add(new go.Point(point.x, point.y));
        });
        newPoints.add(link.getLinkPoint(toNode, toNode.port, go.Spot.Center, false, true, fromNode, fromNode.port));

        link.points = newPoints;
      });

      return;
    }
...

Any ideas on what I’m doing wrong?

Making some progress and this works but throws transaction errors.

[Log] Change not within a transaction: !d points: 27:TEST  old: List()#1670  new: List()#1675 (2.chunk.js, line 287858)
[Log] Change not within a transaction: !m points: 27:TEST  old: List()#1670  new: List()#1675 (2.chunk.js, line 287858)
[Log] Change not within a transaction: !d points: 26:TEST  old: List()#1672  new: List()#1717 (2.chunk.js, line 287858)
[Log] Change not within a transaction: !m points: 26:TEST  old: List()#1672  new: List()#1717 (2.chunk.js, line 287858)

Here is my updated code.

      transitions.forEach(transition => {
        const newPoints = new go.List();
        const link = diagram.findLinkForData(model.findLinkDataForKey(transition.id));
        const fromNode = diagram.findNodeForKey(transition.fromNodeId);
        const toNode = diagram.findNodeForKey(transition.toNodeId);

        newPoints.add(link.getLinkPoint(fromNode, fromNode.port, go.Spot.Center, true, true, toNode, toNode.port));
        transition.points.forEach(point => {
          newPoints.add(new go.Point(point.x, point.y));
        });
        newPoints.add(link.getLinkPoint(toNode, toNode.port, go.Spot.Center, false, true, fromNode, fromNode.port));

        diagram.startTransaction('updatePoints');
        link.points = newPoints;
        diagram.commitTransaction('updatePoints');

        diagram.startTransaction('invalidateRoute');
        link.invalidateRoute();
        diagram.commitTransaction('invalidateRoute');
      });

If I comment out the invalidateRoute() call the transaction errors don’t occur but also my links are N, S, E, W instead of pointing to center of node.

What are the values of Link.fromSpot, Link.toSpot, Link.fromPort.fromSpot, and Link.toPort.toSpot?

{
  "link.fromSpot": {
    "G": null,
    "H": null,
    "Od": -1,
    "Pd": 0,
    "u": true
  },
  "link.toSpot": {
    "G": null,
    "H": null,
    "Od": -1,
    "Pd": 0,
    "u": true
  },
  "link.fromPort.fromSpot": {
    "G": null,
    "H": null,
    "Od": 0,
    "Pd": 0,
    "u": true
  },
  "link.toPort.toSpot": {
    "G": null,
    "H": null,
    "Od": 0,
    "Pd": 0,
    "u": true
  }
}

We have improved the minification process for version 2.1 so that the property names that you see in the debugger for Point, Size, Rect, Margin, and Spot instances are more meaningful than those generated names.

I don’t get how you have two properties that have null values. No matter – don’t worry about that. Try:

link.getLinkPoint(fromNode, fromNode.port, go.Spot.None, true, true, toNode, toNode.port))

And similarly for the toNode/toPort.

I updated to 2.1.3 (looks like I need to generate to license key for that version) and here is the output from that. If I force an error the console says I’m using go-debug.js so don’t know if the output should have friendlier names.

{
  "link.fromSpot": {
    "G": null,
    "H": null,
    "Vd": -1,
    "Wd": 0,
    "s": true
  },
  "link.toSpot": {
    "G": null,
    "H": null,
    "Vd": -1,
    "Wd": 0,
    "s": true
  },
  "link.fromPort.fromSpot": {
    "G": null,
    "H": null,
    "Vd": 0,
    "Wd": 0,
    "s": true
  },
  "link.toPort.toSpot": {
    "G": null,
    "H": null,
    "Vd": 0,
    "Wd": 0,
    "s": true
  }
}

Here is my current code.

        const debug = {
          'link.fromSpot': link.fromSpot,
          'link.toSpot': link.toSpot,
          'link.fromPort.fromSpot': link.fromPort.fromSpot,
          'link.toPort.toSpot': link.toPort.toSpot
        };
        console.log(JSON.stringify(debug));
        newPoints.add(link.getLinkPoint(fromNode, fromNode.port, go.Spot.None, true, true, toNode, toNode.port));
        transition.points.forEach(point => {
          newPoints.add(new go.Point(point.x, point.y));
        });
        newPoints.add(link.getLinkPoint(toNode, toNode.port, go.Spot.None, true, true, fromNode, fromNode.port));

And what my diagram looks like.

56%20AM

Now if I move a node just a hair the links change to how I want them.

image

Ah, you’re right – go-debug.js does not use “x” and “y” as the data member name because those names refer to the actual property getter and setters. For example, the setter in go-debug.js will check the argument value.

Which node did you move “a hair”?

So your call to Link.getLinkPoint at the “Start 1” port returned the point at the bottom of the green circle? That’s odd – that point is not even the closest point between the “Start 1” green circle and the “Activity 2” gray circle. Maybe you need to call Link.getLinkPointFromPoint, passing the first known intermediate point (which in this case might be the only intermediate point that you had loaded).

Moving Activity 2 will correct the link. I tried to upload a video but looks like that’s not possible?

I have it working using the new method and this is my updated code.

    if (event.oldValue === 'Initial Layout' && insertLinkStartEndPoints) {
      transitions.forEach(transition => {
        const newPoints = new go.List();
        const { toNodeId, points, id, fromNodeId } = transition;
        const link = diagram.findLinkForData(model.findLinkDataForKey(id));
        const fromNode = diagram.findNodeForKey(fromNodeId);
        const toNode = diagram.findNodeForKey(toNodeId);

        if (points.length > 0) {
          newPoints.add(
            link.getLinkPointFromPoint(
              fromNode,
              fromNode.port,
              fromNode.port.getDocumentPoint(go.Spot.Center),
              points[0],
              true
            )
          );
          points.forEach(point => {
            newPoints.add(new go.Point(point.x, point.y));
          });
          newPoints.add(
            link.getLinkPointFromPoint(
              toNode,
              toNode.port,
              toNode.port.getDocumentPoint(go.Spot.Center),
              points[points.length - 1],
              false
            )
          );
        }

        diagram.startTransaction('updatePoints');
        link.points = newPoints;
        diagram.commitTransaction('updatePoints');
      });

      return;
    }

Now I’m getting 2 transaction warnings.

Change not within a transaction: !d points: 27:TEST  old: List()#1629  new: List()#1638
Change not within a transaction: !m points: 27:TEST  old: List()#1629  new: List()#1638

I have code to use a transaction but looks like I missed something?

It isn’t clear to me when this code is running. I would do this work in an “InitialLayoutCompleted” DiagramEvent listener.

It’s in a modelChanged event

    if (propertyName === 'CommittedTransaction' || propertyName === 'FinishedUndo' || propertyName === 'FinishedRedo') {
      ...
      this.updateLinks(event, diagram);
      ...
    }

Ah, at that time the transaction has already completed, so of course such modifications will happen outside the transaction. https://gojs.net/latest/intro/changedEvents.html#Transactions

I’m surprised that you would want to execute this code upon undo or redo. I would have expected the need only when loading a model. And for uniformity I would probably keep the design where you only save the intermediate points of each link route, rather than sometimes get complete routes and sometimes only get the intermediate points.

I use modelChanged events for everything now so fixing these points is just an addition to the functionality. I check for event.oldValue === 'Initial Layout' before adding the points and in the calling method propertyName === 'CommittedTransaction' and event.isTransactionFinished === true. My guess is undo and redo won’t have oldValue of ‘Initial Layout’ so I would be OK there.

Since the transaction is finished can I not start a new transaction in a modelChanged event? We actually do it later in that method without any problems.

    // Update GoJS model data
    bindings.forEach(item => {
      diagram.startTransaction('updateModel');
      model.updateTargetBindings(item);
      diagram.commitTransaction('updateModel');
    });

On save our service deletes the start and endpoints so the app only gets midpoints (if they exist)

Yes, I think that should be OK.

So I’m backing to looking at this and when the diagram initially loads I add the start and end points and it’s working great. I’m using a modelChanged event and checking oldValue === ‘Initial Layout’. Now I’m on to the next problem when I save I need add the start/end points again when data comes back from server. I’m using React and in my component update code I actually remove the modelChanged listener.

      diagram.removeModelChangedListener(this.handleModelChanged);
      diagram.startTransaction('modelUpdate');
      model.mergeNodeDataArray(nextProps.nodeDataArray);
      model.mergeLinkDataArray(nextProps.linkDataArray);
      diagram.commitTransaction('modelUpdate');
      diagram.addModelChangedListener(this.handleModelChanged);

I wrote this code 2 years ago and tried commenting out remove listener and bad things happened so I put this in for a reason. Then I noticed your 2.1 code now has a React component and looking at the code it also removes the changed listener so I guess it’s correct to remove it.

So any ideas on how I can detect when the Diagram has new model data? Maybe a Diagram event that will detect both initial layout and updates? I’m going to try some Diagram events but thought I would post to the experts before trying.

So if I understand correctly, you are working in a React environment, and any time React passes in new data via props, you want to append some start and end points to the link data, but not have those changes reflected in your React state?

It sounds like you should modify your componentDidUpdate function to fix up the data how you want it before merging, or you could add a separate modelChangedListener that doesn’t involve calling toIncrementalData/toIncrementalJson.

We use Formik for forms and do update its React state with the points so its data is always in sync with GoJS data.

Once I realized that event.oldValue contained the name of my transaction I got my modelChanged handler to be able to handle both initial load and updates after a save!

      diagram.startTransaction('mergeModelData');
      model.mergeNodeDataArray(nextProps.nodeDataArray);
      model.mergeLinkDataArray(nextProps.linkDataArray);
      diagram.commitTransaction('mergeModelData');

Then in my modelChanged handler I checked for both (this only gets executed on CommittedTransation.

    const { oldValue, model } = event;
    const updatingModel = ['Initial Layout', 'mergeModelData'].includes(oldValue);
    let newTranList;

    if (insertLinkStartEndPoints && updatingModel) {
      transitions.forEach((transition, linkIdx) => {
        const newPoints = new go.List();
        const { toNodeId, points, id, fromNodeId } = transition;
        const linkData = model.findLinkDataForKey(id);
        const link = diagram.findLinkForData(linkData);
        const fromNode = diagram.findNodeForKey(fromNodeId);
        const toNode = diagram.findNodeForKey(toNodeId);

        if (points.length > 0) {
          newPoints.add(
            link.getLinkPointFromPoint(
              fromNode,
              fromNode.port,
              fromNode.port.getDocumentPoint(go.Spot.Center),
              points[0],
              true
            )
          );
          points.forEach(point => {
            newPoints.add(new go.Point(point.x, point.y));
          });
          newPoints.add(
            link.getLinkPointFromPoint(
              toNode,
              toNode.port,
              toNode.port.getDocumentPoint(go.Spot.Center),
              points[points.length - 1],
              false
            )
          );
          updatedTransitions[linkIdx] = {
            ...transitions[linkIdx],
            points: this.mapPoints(newPoints)
          };

          diagram.startTransaction('updatePoints');
          link.points = newPoints;
          diagram.commitTransaction('updatePoints');
        }
      });
    }

Thanks @walter and @jhardy for your help on this. Getting this to work with old data has caused a lot of pain!

Don’t execute transactions within a loop. Move the startTransaction and commitTransaction outside of the iteration.