2012/08/03

To Use TWaver HTML5 to Customize Colorful Links

Recently, some clients have raised the requirement of customizing links, which, I think, is very meaningful. Now I will share with you for your reference. Let’s first see the requirements:
  1. The link is divided into two parts with two colors to fill.
  2. Use different colors to fill different percentages.
  3. Show scales.
  4. Have a switch to control the length of the link. After shortening the link, there are only the beginning part and the ending part (equivalent to a episode of the original link) with no middle part.
  5. If there are more than one links, the parts of the highest percentage will be shown at the two sides of the link after combining.
  6. Before:
    After:
  7. After entering the subnetwork, the information on the link between the node in the subnetwork and the node in the upper layer will show in the subnetwork.
Before entering the network, there is a link between node 2 and node 4 in the subnetwork.
After entering the subnetwork, the link will also show beside node 4.
Let’s first see the effect. Then we will explain how to customize the link after that.
The first 5 requirements can be met by customizing Link and LinkUI. Notice that:
  1. We obtain all bundled links through Link#getBundleLinks.
  2. We draw lines through LinkUI#drawLinePoints.
the complete code:
//customize the constructor function of Link
demo.ScaleLink = function(id, from, to) {
    //call the constructor function of the base class
    demo.ScaleLink.superClass.constructor.call(this, id, from, to);
    //Set the width of the Link as 10 pixes
    this.setStyle('link.width', 10);
    this.setStyle('link.color', 'rgba(0, 0, 0, 0)');
    //Set the type of Link as parallel.
    this.setStyle('link.type', 'parallel');
    //Set the distance between every two links in a bundled link as 40.
    this.setStyle('link.bundle.offset', 40);
    //Set the color of the scales.
    this.setClient('scaleColor', 'black');
    //Set the width of the scales.
    this.setClient('scaleWidth', 1);
    //Set the number the scales.
    this.setClient('scaleNumbers', 4);
    //Set whether the Link is shortened or not.
    this.setClient('shortened', false);
    //Set the length of the link after it is shortened.
    this.setClient('shortenLength', 100);
    //Set the color of the splitter.
    this.setClient('splitterColor', 'black');
    // Set the percentage in the first part of the Link
    this.setClient('fromFillPercent', 0);
    //Set the percentage in the last part of the Link
    this.setClient('toFillPercent', 0);
};
//Set customized Link to inherit from twaver.Link
twaver.Util.ext('demo.ScaleLink', twaver.Link, {
 //reload the method to obtain UI class; returns the customized UI class
    getCanvasUIClass : function () {
        return demo.ScaleLinkUI;
    },
    //obtain the filling color according to the percentage
    getFillColor: function(percent) {
        if (percent < 0.25) {
            return 'green';
        }
        if (percent < 0.5) {
            return 'yellow';
        }
        if (percent < 0.75) {
            return 'magenta';
        }
        return 'red';
    },
    // obtain the filling color in the first part
    getFromFillColor: function () {
        return this.getFillColor(this.getFromFillPercent());
    },
    // obtain the filling color in the last part of the Link
    getToFillColor: function () {
        return this.getFillColor(this.getToFillPercent());
    },
    // obtain the percentage in the first part of the Link
    getFromFillPercent: function () {
        // If there is the bundled link agent between two nodes, the value of the biggest filling percentage in all links in the bundled link returns.
        if (this.isBundleAgent()) {
            var fromAgent = this.getFromAgent(),
                percentKey, maxPercent = 0, percent;
            this.getBundleLinks().forEachSiblingLink(function (link) {
                percentKey = fromAgent === link.getFromAgent() ? 'fromFillPercent' : 'toFillPercent';
                percent = link.getClient(percentKey);
                maxPercent = percent > maxPercent ? percent : maxPercent;
            });
            return maxPercent;
        } else {
            return this.getClient('fromFillPercent');
        }
    },
    // obtain the percentage in the last part of the Link
    getToFillPercent: function () {
        // If there is the bundled link agent between two nodes, the value of the biggest filling percentage in all links in the bundled link returns.
        if (this.isBundleAgent()) {
            var toAgent = this.getToAgent(),
                percentKey, maxPercent = 0, percent;
            this.getBundleLinks().forEachSiblingLink(function (link) {
                percentKey = toAgent === link.getToAgent() ? 'toFillPercent' : 'fromFillPercent';
                percent = link.getClient(percentKey);
                maxPercent = percent > maxPercent ? percent : maxPercent;
            });
            return maxPercent;
        } else {
            return this.getClient('toFillPercent');
        }
    },
    // reload the method to obtain the element name; if there is the bundled link agent between two nodes, the names of the first and the last agent node will return.
    getName: function () {
        if (this.getClient('shortened')) {
            return null;
        } else if (this.isBundleAgent()) {
            return this.getFromAgent().getName() + '-' + this.getToAgent().getName();
        } else {
            return demo.ScaleLink.superClass.getName.call(this);
        }
    }
});

// customize constructor function of LinkUI
demo.ScaleLinkUI = function(network, element){
    // call the constructor function of the type class
    demo.ScaleLinkUI.superClass.constructor.call(this, network, element);
};
// customize Link to inherit twaver.canvas.LinkUI
twaver.Util.ext('demo.ScaleLinkUI', twaver.canvas.LinkUI, {
    // obtain the angle of the Link
    getAngle: function () {
        return getAngle(this.getFromPoint(), this.getToPoint());
    },
    // obtain the central point of the Link
    getMiddlePoint: function (from, to, percent) {
        return {
            x: from.x + (to.x - from.x) * percent,
            y: from.y + (to.y - from.y) * percent
        };
    },
    // draw scale lines
    drawScaleLine: function (from, to, angle, length, ctx, percent, lineWidth, lineColor) {
        var point = this.getMiddlePoint(from, to, percent);
        var y = length/2 * Math.sin(angle),
            x = length/2 * Math.cos(angle);
        ctx.beginPath();
        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.moveTo(point.x + x, point.y + y);
        ctx.lineTo(point.x - x, point.y -y);
        ctx.stroke();
    },
    // obtain whether the link is shortened or not
    isShorten: function () {
        var link = this.getElement();
        return link.getClient('shortened') && this.getLineLength() > link.getClient('shortenLength') * 2;
    },
    // reload the function to draw the link and to draw it by customized logic
    paintBody: function (ctx) {
        var points = this.getLinkPoints(),
            link = this.getElement();
        if (!points || points.size() < 2) {
            return;
        }

        var lineLength = this.getLineLength(),
            shortenLength = link.getClient('shortenLength'),
            percent = shortenLength / lineLength,
            from = points.get(0),
            to = points.get(1),
            angle = this.getAngle() + Math.PI/2;
        if (this.isShorten()) {
            fromPoints = new twaver.List([from, this.getMiddlePoint(from, to, percent)]);
            toPoints = new twaver.List([this.getMiddlePoint(from, to, 1 - percent), to]);
            this._paintBody(ctx, fromPoints, angle);
            this._paintBody(ctx, toPoints, angle);

            // draw texts
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillStyle = 'black';
            var textCenter = {x: (fromPoints.get(0).x + fromPoints.get(1).x)/2, y: (fromPoints.get(0).y + fromPoints.get(1).y)/2};
            ctx.fillText(link.getName(), textCenter.x, textCenter.y);

            textCenter = {x: (toPoints.get(0).x + toPoints.get(1).x)/2, y: (toPoints.get(0).y + toPoints.get(1).y)/2};
            ctx.fillText(link.getName(), textCenter.x, textCenter.y);

            ctx.fillText(link.getToNode().getName(), fromPoints.get(1).x, fromPoints.get(1).y);
            ctx.fillText(link.getFromNode().getName(), toPoints.get(0).x, toPoints.get(0).y);
        } else {
            this._paintBody(ctx, points, angle);
        }

        // draw the first arrow
        if (link.getClient('arrow.from')) {
            twaver.Util.drawArrow(ctx, 12, 9, points, true, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
        }
        // draw the last arrow
        if (link.getClient('arrow.to')) {
            twaver.Util.drawArrow(ctx, 12, 9, points, false, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
        }
    },
    _paintBody: function (ctx, points, angle) {
        var link = this.getElement(),
            width = link.getStyle('link.width'),
            grow = width,
            outerColor = this.getOuterColor();
        if (outerColor) {
            var outerWidth = link.getStyle('outer.width');
            grow += outerWidth * 2;
        }
        var selectBorder = !this.getEditAttachment() && link.getStyle('select.style') === 'border' && this.getNetwork().isSelected(link);
        if (selectBorder) {
            var selectWidth = link.getStyle('select.width');
            grow += selectWidth * 2;
        }
        ctx.lineCap = link.getStyle('link.cap');
        ctx.lineJoin = link.getStyle('link.join');
        // draw the selected outline
        if (selectBorder) {
            this.drawLinePoints(ctx, points, grow, link.getStyle('select.color'));
        }
        // draw the outline
        if (outerColor) {
            this.drawLinePoints(ctx, points, width + outerWidth * 2, outerColor);
        }
        // draw the Link
        this.drawLinePoints(ctx, points, width, this.getInnerColor() || link.getStyle('link.color'));

        var fromFillPercent = link.getFromFillPercent(),
            toFillPercent = link.getToFillPercent(),
            fromFillColor = link.getFromFillColor(),
            toFillColor = link.getToFillColor(),
            from = points.get(0),
            to = points.get(1);

        var x = from.x + (to.x - from.x) / 2 * fromFillPercent,
            y = from.y + (to.y - from.y) / 2 * fromFillPercent;
        var middle = {x: x, y: y};
        var fromPoints = new twaver.List([from, middle]);
        // draw filling color of the first part
        this.drawLinePoints(ctx, fromPoints, width, fromFillColor);

        from = points.get(1);
        to = points.get(0);
        x = from.x + (to.x - from.x) / 2 * toFillPercent;
        y = from.y + (to.y - from.y) / 2 * toFillPercent;
        middle = {x: x, y: y};
        var toPoints = new twaver.List([from, middle]);
        // draw filling color of the last part
        this.drawLinePoints(ctx, toPoints, width, toFillColor);

        from = points.get(0);
        to = points.get(1);
        var scaleWidth = link.getClient('scaleWidth'),
            scaleColor = link.getClient('scaleColor');
        // draw the scales
        for (var i = 1, n = link.getClient('scaleNumbers') * 2; i < n; i++) {
            this.drawScaleLine(from, to, angle, width/2, ctx, i/n, scaleWidth, scaleColor);
        }
        // draw the spiltter
        this.drawScaleLine(from, to, angle, width, ctx, 0.5, 3, link.getClient('splitterColor'));
    }
});
The last requirement can be met by customizing Node and NodeUI.
 // customize the constructor function of Node
demo.ScaleNode = function(id) {
    // call the constructor function of the base class
    demo.ScaleNode.superClass.constructor.call(this, id);
};
// set the customized Node to inherit from twaver.Nodes
twaver.Util.ext('demo.ScaleNode', twaver.Node, {
    getCanvasUIClass: function () {
        return demo.ScaleNodeUI;
    }
});

// customize the constructor function of NodeUI
demo.ScaleNodeUI = function(network, element){
    // call the constructor function of the base class
    demo.ScaleNodeUI.superClass.constructor.call(this, network, element);
};
// set the customized NodeUI to inherit from twaver.canvas.NodeUI
twaver.Util.ext('demo.ScaleNodeUI', twaver.canvas.NodeUI, {
    // reload the method to draw the element and draw the link to the upper layer
    paintBody: function (ctx) {
        demo.ScaleNodeUI.superClass.paintBody.call(this, ctx);
        var result = this.getAttachedLinks();
        if (!result) {
            return;
        }
        for (var position in result) {
            this.paintLink(ctx, result[position], position);
        }
    },
    // draw the link
    paintLink: function (ctx, links, position) {
        var center = this.getElement().getCenterLocation(),
            count = links.length,
            half = count / 2,
            network = this.getNetwork(),
            gap = (count - 1) * -10,
            terminal, link, i, offset, shortenLength, angle, tempCenter, textWidth, textHeight = 20, textCenter;
        for (i=0; i<count; i++) {
            link = links[i];
            offset = link.getStyle('link.bundle.offset');
            shortenLength = link.getClient('shortenLength');
            textWidth = ctx.measureText(link.getName()).width;
            if (position === 'left') {
                terminal = {x: center.x - offset - shortenLength, y: center.y + gap};
                tempCenter = {x: center.x - offset, y: center.y + gap};
                textCenter = {x: terminal.x - textWidth/2 - 10, y: terminal.y};
                angle = Math.PI/2;
            } else if (position === 'right') {
                terminal = {x: center.x + offset + shortenLength, y: center.y + gap};
                tempCenter = {x: center.x + offset, y: center.y + gap};
                textCenter = {x: terminal.x + textWidth/2 + 10, y: terminal.y};
                angle = Math.PI/2;
            } else if (position === 'top') {
                terminal = {x: center.x + gap, y: center.y - offset - shortenLength};
                tempCenter = {x: center.x + gap, y: center.y - offset};
                textCenter = {x: terminal.x, y: terminal.y - 10};
                angle = 0;
            } else {
                terminal = {x: center.x + gap, y: center.y + offset + shortenLength};
                tempCenter = {x: center.x + gap, y: center.y + offset};
                textCenter = {x: terminal.x, y: terminal.y + 10};
                angle = 0;
            }
            gap += 20;
            var isFrom = link.getFromNode() === this.getElement(),
                points;
            if (isFrom) {
                points = new twaver.List([tempCenter, terminal]);
            } else {
                points = new twaver.List([terminal, tempCenter]);
            }
            network.getElementUI(link)._paintBody(ctx, points, angle);

            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillStyle = 'black';
            // the label of the node at another side
            var name = isFrom ? link.getToNode().getName() : link.getFromNode().getName();
            ctx.fillText(name, textCenter.x, textCenter.y);
            textCenter = {x: (tempCenter.x + terminal.x)/2, y: (tempCenter.y + terminal.y)/2};
            // the label of Link
            ctx.fillText(link.getName(), textCenter.x, textCenter.y);

            // draw the first arrow
            if (link.getClient('arrow.from')) {
                twaver.Util.drawArrow(ctx, 12, 9, points, true, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
            }
            // draw the last arrow
            if (link.getClient('arrow.to')) {
                twaver.Util.drawArrow(ctx, 12, 9, points, false, 'arrow.standard', true, 'gray', 0, 0, 1, 'black');
            }
        }
    },
    // obtain the collection of the links to the upper layer of different directions
    getAttachedLinks: function () {
        var currentSubNetwork = this.getNetwork().getCurrentSubNetwork();
        if (!currentSubNetwork || !this.getElement().getLinks()) {
            return null;
        }
        var result;
        this.getElement().getLinks().forEach(function (link) {
            var fromSubNetwork = twaver.Util.getSubNetwork(link.getFromNode()),
                toSubNetwork = twaver.Util.getSubNetwork(link.getToNode());
            if (fromSubNetwork !== toSubNetwork) {
                if (!result) {
                    result = {};
                }
                var fromCenter = link.getFromNode().getCenterLocation(),
                    toCenter = link.getToNode().getCenterLocation(),
                    angle = getAngle(fromCenter, toCenter),
                    isOut = currentSubNetwork === fromSubNetwork,
                    position;
                if (isOut) {
                    if (fromCenter.x <= toCenter.x) {
                        if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
                            position = 'right';
                        } else if (angle > Math.PI/4) {
                            position = 'bottom';
                        } else {
                            position = 'top';
                        }
                    } else {
                        if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
                            position = 'left';
                        } else if (angle > Math.PI/4) {
                            position = 'top';
                        } else {
                            position = 'bottom';
                        }
                    }
                } else {
                    if (fromCenter.x <= toCenter.x) {
                        if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
                            position = 'left';
                        } else if (angle > Math.PI/4) {
                            position = 'top';
                        } else {
                            position = 'bottom';
                        }
                    } else {
                        if (angle >= -Math.PI/4 && angle <= Math.PI/4) {
                            position = 'right';
                        } else if (angle > Math.PI/4) {
                            position = 'bottom';
                        } else {
                            position = 'top';
                        }
                    }
                }
                if (!result[position]) {
                    result[position] = [];
                }
                result[position].push(link);
            }
        });
        return result;
    }
});

No comments:

Post a Comment