2012/10/25

To Build Outdoor Scenes Topology with TWaver 3D

It is quite common in general application system to present the inside of a machine room by means of 2D which is characteristic of being efficient, direct-viewing and exact. With the increase of the amount of information and the development of the technology, clients hope that there can be a more realistic 3D scene to make it more readable. TWaver Flex 3D has realized the representation of 3D scenes with the Element objects in TWaver, by expanding 3D Style of them.

Among all the requirements, the commonest one is to create an array of devices with regular shapes in a machine room and to automatically construct a cabinet with the specified rows and columns.
This standard scene can be constructed through the following codes easily:
public static function getRoomBox(id:Object):ElementBox {
            var size:Number=1600;
            var box:ElementBox=new ElementBox();
            var xgap:Number=size / 8;
            var ygap:Number=size / 8;
            var startX:Number=-xgap * 2.5;
            var starty:Number=-ygap * 2.5;
            for (var i:int=0; i < 6; i++)
            {
                for (var j:int=0; j < 6; j++)
                {
                    box.add(createRack(i + ":" + j, startX + i * xgap, starty + j * xgap));
                }
            }

            var floor:IElement=new Element();

            floor.setStyle(Style3D.MAPPINGTYPE, Consts3D.MAPPINGTYPE_COLOR);
            floor.setStyle(Style3D.THREED_SHAPE_TYPE, Consts3D.THREED_SHAPE_TYPE_CUBE);
            floor.setStyle(Style3D.MATERIAL_ALPHA, 0.6);
            floor.setStyle(Style3D.MATERIAL_COLOR, 0x999999);
            floor.setStyle(Style3D.ZORDERING_OFFSET, 2500);

            floor.setStyle(Style3D.BOTH_SIDES_VISIBLE, true);
            floor.setStyle(Style3D.PROPERTY_SPACE_LOCATION, new Vector3D(0, 0, 0));
            floor.setStyle(Style3D.PROPERTY_SIZE, new Vector3D(size + 10, 0, size + 10));
            box.add(floor);

            return box;
}
private static function createRack(id:String, x:Number, z:Number):IElement{
            var element:IElement=new Element(id);
            element.setStyle(Style3D.MAPPINGTYPE, Consts3D.MAPPINGTYPE_MAP6);
            element.setStyle(Style3D.MAPPING_COMMON_PATH, "rack3DImage");
            element.setStyle(Style3D.PROPERTY_SIZE, new Vector3D(80, 150, 80));
            element.setStyle(Style3D.PROPERTY_SPACE_LOCATION, new Vector3D(x, 150 / 2 + 10, z));
            return element;
}
After the indoor scene, clients have a new requirement that it will be better if the outside of the room can be seen, such as the representation of the connection between the room and another one. To satisfy it, we have to transfer the scene to the outside to present the storey and the connection between it and another. The following codes can help us realize that:
  public static function buildSpacialTopo(box:ElementBox):void{
                var o:Vector3D = new Vector3D(0,0,0);
                var cbp:Vector3D = new Vector3D(0,50,0);
                var lup:Vector3D = new Vector3D(100,10,-60);
                var rup:Vector3D = new Vector3D(-100,10,-60);
                var fp:Vector3D = new Vector3D(0,10,100);
                var l1:Node = createLine([o,new Vector3D(rup.x,3,0),new Vector3D(rup.x,3,rup.z)],5,0x000000);
                box.add(l1);
                l1.name = "black line";
                var l2:Node = createLine([o,new Vector3D(0,3,lup.z),new Vector3D(lup.x,3,lup.z)],5,0x00FF00);
                l2.name = "green line";
                box.add(l2);
                var l3:Node = createLine([o,new Vector3D(fp.x,2,fp.z)],3,0xFF0000);
                l3.name = "red line";
                box.add(l3);
                var cb:Node = createCube(cbp,"BI",new Vector3D(50,100,30));
                cb.name = "building";
                box.add(cb);
                var ds1:Node = createCube(lup,"NO",new Vector3D(25,40,10));
                ds1.name = "NO.1";
                box.add(ds1);
                var ds2:Node = createCube(rup,"NT",new Vector3D(35,20,10));
                ds2.name = "No.2";
                box.add(ds2);
                var f:Node = createCube(fp,"SM",new Vector3D(20,35,10));
                f.setStyle(Style3D.PROPERTY_ROT_ANGLE,new Vector3D(0,180,0));
                f.name = "SM";
                box.add(f);
}
public static function createLine(ps,width,color):Node{
                var line:Node = new Node();
                line.setStyle(Style3D.THREED_SHAPE_TYPE,Consts3D.THREED_SHAPE_TYPE_LINE);
                line.setStyle(Style3D.LINE_SHAPE_POINTS,ps);
                line.setStyle(Style3D.MAPPINGTYPE,Consts3D.MAPPINGTYPE_COLOR);
                line.setStyle(Style3D.MATERIAL_COLOR,color);
                line.setStyle(Style3D.LINE_SHAPE_WIDTH,width);
                return line;
}
public static function createCube(pos,mappingsource,size):Node {
                var node:Node = new Node();
                node.setClient("TYPE","building");
                node.setStyle(Style3D.PROPERTY_SPACE_LOCATION,pos);
                node.setStyle(Style3D.PROPERTY_SIZE,size);
                if(mappingsource!=null){
                    node.setStyle(Style3D.MAPPINGTYPE,Consts3D.MAPPINGTYPE_MAP6);
                    node.setStyle(Style3D.MAPPING_COMMON_PATH,mappingsource);
                }else{
                    node.setStyle(Style3D.MAPPINGTYPE,Consts3D.MAPPINGTYPE_COLOR);
                    node.setStyle(Style3D.MATERIAL_COLOR,0x33FF88);
                }

                return node;
}
We use the 3D model of Line to simulate the linking between these architectures. Let’s have a look:
When we are smooth in applications, clients again have a new idea: Now there have been the indoor and outdoor scenes, but in the outdoor one the architectures are alone and too simple, so the geographical relationship between the architectures is invisible. Can there be more functions like this? O-K. We have to serve clients, to meet the changeable requirements as long as we are coders.

Why not load the simulated the altitude data?
public static function buildTerrain(box:ElementBox):void{
                var node:Node = new Node();
                node.setStyle(Style3D.THREED_SHAPE_TYPE,Consts3D.THREED_SHAPE_TYPE_TERRAIN);
                node.setStyle(Style3D.MAPPINGTYPE,Consts3D.MAPPINGTYPE_COMMON);
                node.setStyle(Style3D.MAPPING_COMMON_PATH,"TT");
                node.setStyle(Style3D.MAPPING_HEIGHTMAP,"HM");
                node.setStyle(Style3D.EXTRUSION_SAMPLES,70);
                node.setStyle(Style3D.EXTRUSION_XSCALE,1);
                node.setStyle(Style3D.EXTRUSION_YSCALE,1);
                node.setStyle(Style3D.EXTRUSION_RECENTER,true);
                node.setStyle(Style3D.PROPERTY_ROT_ANGLE,new Vector3D(90,0,0));
                node.setStyle(Style3D.EXTRUSION_HSCALE,0.1);
                node.setStyle(Style3D.PROPERTY_SPACE_LOCATION,new Vector3D(-100,-100,0));
                node.setClient("TYPE","terrain");
                box.add(node);
}
Now we are able to simulate their geographical relations.


Life is colorful with a series of unexpectedness. A new requirement comes again: How do I operate? How do I interact with it? How do I lead my users to enter the computer room, operate the cabinet, and check the devices and the panes in the room?

Yeah, we have to combine these elements together. Then we add the interactional system. When a user clicks a storey, there will pop out the indoor scene of the machine room in which he can select the cabinet; when double-clicking that cabinet, there appears the 3D model of the devices in it; when double-clicking a device I can again see its panes.

network.selectionFunction = function(element:Element){
        if("terrain" == element.getClient("TYPE")){
            return false;
        }
        return true;
}
network.addEventListener(MouseEvent.MOUSE_MOVE,onClick);
                               .............
private function onSelectionChanged(evt:SelectionChangeEvent):void{
                hideShapedWindow();
                level = 0;
                b1 = null;
                b2 = null;
                var data:Element = box.selectionModel.lastData as Element;
                if(box.selectionModel.selection.contains(data)){
                    showRoomInfo(data,this.mousePos);
                }
}
private function onClick(evt:MouseEvent):void{
                var pos:Point = new Point(evt.stageX,evt.stageY);
                this.mousePos = network.globalToContent(pos);
            }
private function showRoomInfo(element:Element,pos:Point):void{
                if("building" == element.getClient("TYPE")){
                    var box:ElementBox =DataUtils.getRoomBox("fd");
                    room = createRoom();
                    showShapedWindow(room,pos);
                }
}

 

2012/10/18

Partial AutoLayout and Layout in Nested Groups in TWaver

TWaver has provided some layout algorithms to help us quickly set the positions for elements. They are enough for us to use in many cases, but in some complicated ones we need to make some operations ourselves. Please look at the following example:
Screenshot:
Under this circumstance, it is not so agreeable to use TNetwork#doLayout directly, for doLayout will use the same layout algorithm to all elements in the network. But the several sub-groups under the group in the screenshot have their own layout. My solution is that we can first arrange the layout wholly to the group at the outermost layer and then recurse and go through the sub-groups, namely, arrange the layout to them separately.

Code Implementation:
There are two ways available to use the partial layout in TWaver:
  • Add the Element to be in the layout into a new TNetwork, and arrange the layout with the new TNetwork.
  • TWaver only arranges the layout to the selected element as default, so the element to be in the layout can be put in to the SelectionModel.
In this demo, at first, use the first way above to arrange the layout to the outermost group:
//layoutNetwork is a temporary network which is used to arrange a layout.
//The layout has been saved in ClientProperty when generating the group.
layoutDatabox.addElementWithDescendant(group1);
layoutNetwork.doLayout((Integer)group1.getClientProperty("layout"),false);
Then recurse and go through the sub-groups to arrange the layout to them.
recursionChild(group1);
............................................

 private void recursionChild(Group group){
        for(int i=0;i<group.getChildren().size();i++){
            Element element= (Element) group.getChildren().get(i);
            if(element instanceof Group){
                Group childGroup=((Group)element);
                recursionChild(childGroup);//If there are sub-groups, the recursion will be continued.
//to combine the two ways together and add the sub-group to be in a layout into the SelectonModel of layoutNetwork
                layoutDatabox.getSelectionModel().clearSelection();
                layoutDatabox.getSelectionModel().appendSelection(childGroup.getChildren());
                layoutNetwork.doLayout((Integer) ((Group) element).getClientProperty("layout"), false);
                layoutDatabox.getSelectionModel().clearSelection();
            }
        }
    }
All codes:
public class LayoutTest extends JPanel {
    TDataBox layoutDatabox=new TDataBox();
    TNetwork layoutNetwork=new TNetwork(layoutDatabox);
    public LayoutTest(){

        TNetwork network=new TNetwork();

        ResizableNode gatewayNode=new ResizableNode();

        gatewayNode.putCustomDraw(true);
        gatewayNode.putCustomDrawShapeFactory(TWaverConst.SHAPE_ROUND_RECTANGLE_HALF);
        gatewayNode.putCustomDrawGradient(false);
        gatewayNode.putCustomDrawFillColor(new Color(254, 254, 206));
        gatewayNode.setSize(70, 30);
        gatewayNode.setName("Gateway");
        gatewayNode.putLabelPosition(TWaverConst.POSITION_CENTER);

        Group group1=createGroup(4,TWaverConst.LAYOUT_TREE);
        Group group1_1=createGroup(12,TWaverConst.LAYOUT_CIRCULAR);
        Group group1_2=createGroup(5,TWaverConst.LAYOUT_EAST);

        Link group1_2Link=new Link((Node)group1.getChildren().get(0),(Node)group1_2.getChildren().get(0));
        setLinkStyle(group1_2Link);
        group1.addChild(group1_2);
        group1.addChild(group1_2Link);

        Link group1_1Link=new Link((Node)group1.getChildren().get(0),(Node)group1_1.getChildren().get(0));
        setLinkStyle(group1_1Link);
        group1.addChild(group1_1);
        group1.addChild(group1_1Link);

//layoutNetwork is a temporary network which is used to arrange the layout.
//The layout has been saved in ClientProperty when generating the group.
        layoutDatabox.addElementWithDescendant(group1);
        layoutNetwork.doLayout((Integer)group1.getClientProperty("layout"),false);
        recursionChild(group1);

        TDataBox box=new TDataBox();
        box.addElementWithDescendant(group1);
        box.addElement(gatewayNode);

        group1.putGroupOpaque(true);
        group1.putGroupFillColor(new Color(154,206,254));
        network.setDataBox(box);
        this.setLayout(new GridLayout(1,3));
        this.add(network);

        Layer layer=new Layer();
        box.getLayerModel().addLayer(layer);
        box.getLayerModel().moveToTop(layer);

        Link link1=new Link(group1,gatewayNode);
        setLinkStyle(link1);
        link1.setLinkType(TWaverConst.LINK_TYPE_ORTHOGONAL);
        link1.setLayerID(layer.getID());
        box.addElement(link1);
    }

    /**
     * arrange the layout:to recurse the nested group
     * @param group
     */
    private void recursionChild(Group group){
        for(int i=0;i<group.getChildren().size();i++){
            Element element= (Element) group.getChildren().get(i);
            if(element instanceof Group){
                Group childGroup=((Group)element);
                recursionChild(childGroup);//If there are sub-groups, the recursion will be continued.
//to combine the two ways together and add the sub-group to be in a layout into the SelectonModel of layoutNetwork
                layoutDatabox.getSelectionModel().clearSelection();
                layoutDatabox.getSelectionModel().appendSelection(childGroup.getChildren());
                layoutNetwork.doLayout((Integer) ((Group) element).getClientProperty("layout"), false);
                layoutDatabox.getSelectionModel().clearSelection();
            }
        }
    }
    /**
     * to generate the test group and node
     * @param childCount
     * @param layout
     * @return
     */
    private Group createGroup(int childCount,int layout){
        Group group=new Group();
        group.setExpand(true);
        group.putClientProperty("layout",layout);

        ResizableNode router=new ResizableNode();
        router.putCustomDraw(true);
        router.putCustomDrawShapeFactory(TWaverConst.SHAPE_RECTANGLE);
        router.putCustomDrawGradient(false);
        router.putCustomDrawFillColor(new Color(254, 254, 206));
        router.setSize(60, 20);
        router.setName("Router");
        router.putLabelPosition(TWaverConst.POSITION_CENTER);
        group.addChild(router);

        for(int i=0;i<childCount;i++){
            Node childNode=new Node();
            Link link=new Link(router,childNode);
            childNode.putCustomDraw(true);
            childNode.putCustomDrawShapeFactory(TWaverConst.SHAPE_RECTANGLE);
            childNode.putCustomDrawGradient(false);
            childNode.putCustomDrawFillColor(new Color(254,202,2));
            childNode.setName("C"+i);
            childNode.putLabelPosition(TWaverConst.POSITION_CENTER);

            setLinkStyle(link);
            group.addChild(childNode);
            group.addChild(link);
        }
        return group;
    }

    /**
     * to set the style of the link
     * @param link
     */
    public void setLinkStyle(Link link){
        link.putLinkColor(Color.black);
        link.putLinkOutlineWidth(0);
        link.putLinkWidth(1);
        link.putLinkAntialias(true);
        link.putLinkToArrow(true);
        link.putLinkToArrowOutline(false);
    }
    public static void main(String[] args){
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame=new JFrame("layout");
                frame.setSize(1000,700);
                frame.setContentPane(new LayoutTest());
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}

2012/10/12

TWaver With JavaFX

We have paid little attention on JavaFX since JavaFX Script was not provided in Oracle. However, now we have found that in fact Oracle has attached much importance on it, for we have discovered by chance that JavaFX has developed to version 2.2 and been binding to JDK7.、
Recently I have seen someone putting up questions in our forum and asked whether there is JavaFX version in TWaver, so I have had a browse on Oracle’s website and found that it is applicable to embed JavaFX into Swing. It is quite simple and convenient to do that so I have written a Demo right away.

Effects

An Analysis on the code
There is a typical Network in the middle of the window: there is a JavaFX container in the top and bottom respectively. The top one includes a Slider, sliding which can change the RenderColor of the selected Node in TWaver. Some texts are involved in the bottom container. The name and the location of a node will be shown on the Text component when selecting and dragging the node. 

In this Demo, Swing is still the application architecture. JavaFX has provided a component named JFXpanel which can be embedded into a Swing container and JavaFX components can be put into the JFXPanel. However, attention need to be paid to the difference in the thread of Swing and JavaFX(the official text says:” JavaFX data should be accessed only on the JavaFX User thread.” and “Swing data should be changed only on the EDT.” ). Therefore, if you want to change the data of JavaFX in Swing, the encapsulation of Platform#runLater is suggested.

The code for the Text component of JavaFX that is changed when dragging a node in this example:

network.getDataBox().addElementPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if("location".equals(evt.getPropertyName())){
                    Element element= (Element) evt.getSource();
                    final String name=element.getName();
                    final double x=element.getX();
                    final double y=element.getY();
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            nameText.setText(name);
                            locationText.setText("X:"+x+"   Y:"+y);
                        }
                    });
                }
            }
        });
Likewise, if you want to change the Swing data in JavaFX, the encapsulation of SwingUtilities#invokeLater is suggested.
The following is the code for the Render Color of the selected node which is changed when dragging the Slider in this example:
slider.valueProperty().addListener(new ChangeListener<Number>() {
            public void changed(ObservableValue<? extends Number> ov,
                                Number old_val,final Number new_val) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        Iterator it=network.getSelectionModel().selection();
                        while (it.hasNext()){
                            Element element= (Element) it.next();
                            element.putRenderColor(new java.awt.Color(new_val.intValue()));
                        }
                    }
                });
            }
        });
 All codes:

public class Test1 {
    static TNetwork network;
    static Text nameText;
    static Text locationText;
    static  Slider slider;
    private static void initAndShowGUI() {
        initNetwork();
        JFrame frame = new JFrame("TWaver With JavaFX");
        frame.getContentPane().setLayout(new BorderLayout());

        frame.setSize(500, 300);
        frame.setVisible(true);
        frame.getContentPane().add(network);

        final JFXPanel controlFXPanel = new JFXPanel();
        frame.add(controlFXPanel,BorderLayout.NORTH);

        final JFXPanel statusFXPanel = new JFXPanel();
        frame.add(statusFXPanel,BorderLayout.SOUTH);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        network.getDataBox().addElementPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if("location".equals(evt.getPropertyName())){
                    Element element= (Element) evt.getSource();
                    final String name=element.getName();
                    final double x=element.getX();
                    final double y=element.getY();
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            nameText.setText(name);
                            locationText.setText("X:"+x+"   Y:"+y);
                        }
                    });
                }
            }
        });
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                initFX(statusFXPanel,controlFXPanel);
            }
        });
    }
    private static void initNetwork(){
        network=new TNetwork();
        twaver.Node node1=new Node();
        node1.setName("node1");
        twaver.Node node2=new Node();
        node2.setLocation(200,100);
        node2.setName("node2");
        Link link=new Link(node1,node2);
        network.getDataBox().addElement(node1);
        network.getDataBox().addElement(node2);
        network.getDataBox().addElement(link);

    }
    private static void initFX(JFXPanel statusFXPanel,JFXPanel controlFXPanel) {
        Scene scene = createStatusScene();
        statusFXPanel.setScene(scene);
        statusFXPanel.validate();

        Scene controlScene=createControlScene();
        controlFXPanel.setScene(controlScene);
        controlFXPanel.validate();

    }
    private static Scene createControlScene(){
        Group  root  =  new  Group();
        Scene  scene  =  new  Scene(root, Color.ALICEBLUE);
        HBox hbox=new HBox();
        slider =new Slider(0,16777215,0);
        slider.setPrefHeight(30);
        slider.setPrefWidth(300);
        slider.valueProperty().addListener(new ChangeListener<Number>() {
            public void changed(ObservableValue<? extends Number> ov,
                                Number old_val, final Number new_val) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        Iterator it = network.getSelectionModel().selection();
                        while (it.hasNext()) {
                            Element element = (Element) it.next();
                            element.putRenderColor(new java.awt.Color(new_val.intValue()));
                        }
                    }
                });
            }
        });
        Label label=new Label("颜色:");
        label.setPrefHeight(30);
        hbox.getChildren().add(label);
        hbox.getChildren().add(slider);
        root.getChildren().add(hbox);
        return scene;
    }
    private static Scene createStatusScene() {
        Group  root  =  new  Group();
        Scene  scene  =  new  Scene(root, Color.ALICEBLUE);
        VBox vbox=new VBox();
        vbox.setPrefWidth(300);
        vbox.setPrefHeight(20);
        HBox hbox=new HBox();
        nameText=new Text();
        hbox.getChildren().add(new Label("  Name:"));
        hbox.getChildren().add(nameText);
        hbox.getChildren().add(new Text("   "));
        locationText=new Text();
        hbox.getChildren().add(new Label("Location:"));
        hbox.getChildren().add(locationText);
        vbox.getChildren().add(hbox);
        root.getChildren().add(vbox);
        return (scene);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                initAndShowGUI();
            }
        });
    }
}

 

2012/09/28

Popup Menu Realized by the New Functions in Flash Player 11.2

In Flash Player 11.2, a significant function has been added: the Popup Menu. Please refer to Flash Player 11.2 Beta Features for details.  The following image is the effect in the Network in TWaver Flex which means that it is unnecessary to screen out the Popup Menu through the js script of html, which method no longer takes effect after the content in flash is full-screening. Because after that, flash is out of the browser and it is of little help to listen to the right button in the browser.
Attentions need to be paid to:
  1. Click here to download Flash Player 11.2 and playerglobal.swc file
  2. Change the name of the downloaded file flashplayer11-2_p3_playerglobal_122011.swc to playerglobal.swc and put it into the corresponding catalogue of SDK(Adobe Flash Builder 4.5/sdks/4.5.1/frameworks/libs/player/11.2)
  3. SDK 4.5 or the edition above is suggested. (The edition below SDK4.0 is not compatible with Flash Player 11.)
  4. Set the edition of Flash Player in the project options as 11.2.
  5. Add the option -swf-version=15 in the project options.
The code:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               xmlns:twaver="http://www.servasoftware.com/2009/twaver/flex"
               applicationComplete="init()">
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            import mx.controls.Menu;

            import twaver.*;

            private function init():void {
                var box:ElementBox = new ElementBox();

                var from:Node = new Node();
                from.name = "From";
                from.location = new Point(100, 100);
                box.add(from);

                var to:Node = new Node();
                to.name = "To";
                to.location = new Point(300, 300);
                box.add(to);

                var link:Link = new Link(from, to);
                link.name = "From - To";
                box.add(link);

                network.elementBox = box;
                network.addEventListener('rightClick', handleRighClick);
            }

            private var menu:Menu = null;

            private function handleRighClick(e:MouseEvent):void {
                var element:IElement = network.getElementByMouseEvent(e);
                var myMenuData:ArrayCollection = new ArrayCollection([
                    {label: element == null ? "none" : element.name}
                ]);
                if(menu != null){
                    menu.hide();
                }
                menu = Menu.createMenu(network, myMenuData, false);
                var point:Point = network.globalToLocal(new Point(e.stageX, e.stageY));
                menu.show(point.x, point.y);
            }
        ]]>
    </fx:Script>

    <twaver:Network id="network" width="100%" height="100%" backgroundAlpha="0" backgroundColor="#FF0000"/>
</s:Application>
 

2012/09/27

An Example on the Application of HTML5 WebSocket

After the article An introduction on HTML5 WebSocket, this one will explain the use of WebSocket with an example in which we have combined TWaver HTML5: the background provides the topological data which is sent to every client-side through WebSocket in the form of JSON and after it is received by them it will be shown on the interface by the Network components in TWaver HTML5. The elements can be handled at the client-sides, the results of which are sent to the background through WebSocket, and then the background server will renew and notify all client-sides to refresh their interfaces. After that, there will also be alarms appearing constantly in the background server which will be sent to the client-sides to refresh their interfaces.
General structures
Preparations
You can download jetty and twaver html5 which are necessary in this example.
jetty:http://www.eclipse.org/jetty/

The Directory Structure of Jetty
The following structure is what jetty is decompressed. Run start.jar(java -jar start.jar) to start jetty server(java -jar start.jar)and web items can be put into the catalogue “/webapps”, such as “/webapps/alarm/” in this example.

The Background
Jetty is used at the background whose style is the same as the api of servlet. It can be used according to the use and deployment of Servlet. There are three classes needed to be used in this example.
  • WebSocketServlet-WebSocket Service Class
  • WebSocket–corresponding to a WebSocket client-side
  • WebSocket.Conllection–stands for a WebSocket connection
WebSocketServlet
The full name is org.eclipse.jetty.websocket.WebSocketServlet which is used to provide websocket service. It inherits from HttpServlet and the method public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) has been added. This method is called when the client-side asks for websocket connection for the first time. If it is allowed to establish a connection, then returns a WebSocket instance object. Otherwise returns null.
In this example, we will define an AlarmServlet class which inherits from WebSocketServlet to realize the method "doWebSocketConnect"and returns an "AlarmWebSocket" instance which stands for a client-side.

AlarmServlet
There is a “clients” property in AlarmWebSocket which is used to maintain a client-side (AlarmWebSocket) list. When establishing a connection with the client-side, the corresponding AlarmWebSocket instance of the client-side will be added to the list; when the client-side is closed, it will be deleted from the list.
public class AlarmServlet extends org.eclipse.jetty.websocket.WebSocketServlet {
    private final Set<AlarmWebSocket> clients;//to save the client-side list

    public AlarmServlet() {
        initDatas();//to initialize the data
    }

    @Override
    public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
        return new AlarmWebSocket();
    }
    //...
}
AlarmWebSocket
Let’s see AlarmWebSocket. Here an Inner Class has been defined to realize the three methods of the interface org.eclipse.jetty.websocket.WebSocket.OnTextMessage: onOpen/onMessage/onClose which are called back when establishing connections, receiving messages from client-sides and closing connections respectively. If you have the need to send messages to the client-sides, you can call the method Connection#sendMessage(…) .(The message should all be JSON.) 
      class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
       WebSocket.Connection connection;
    @Override
    public void onOpen(Connection connect) {
        this.connection = connect;
        clients.add(this);
        sendMessage(this, "reload", loadDatas());
    }
    @Override
    public void onClose(int code, String message) {
        clients.remove(this);
    }
    @Override
    public void onMessage(String message) {
        Object json = JSON.parse(message);
        if(!(json instanceof Map)){
            return;
        }
        //to analyze the message: the json data in jetty will be analyzed into map objects
        Map map = (Map)json;
        //to renew the data model at the background through the information in the massages
        ...
        //to process the messages to notify other client-sides
        for(AlarmWebSocket client : clients){
            if(this.equals(client)){
                continue;
            }
            sendMessage(client, null, message);
        }
    }
}
private void sendMessage(AlarmWebSocket client, String action, String message){
    try {
        if(message == null || message.isEmpty()){
            message = "\"\"";
        }
        if(action != null){
            message = "{\"action\":\"" + action + "\", \"data\":" + message + "}";
        }
        client.connection.sendMessage(message);
    } catch (IOException e) {
        e.printStackTrace();
    }
}


Background Configuration
The background configuration is the same as serlvet. Set the url as /alarmServer.

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    metadata-complete="false"
    version="3.0">
    <servlet>
        <servlet-name>alarmServlet</servlet-name>
        <servlet-class>web.AlarmServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>alarmServlet</servlet-name>
        <url-pattern>/alarmServer</url-pattern>
    </servlet-mapping>
</web-app>
 The foreground part
Let’s have a look at the general structure of foreground. Now we can create “websocket” connection to listen to the related events like “onmessage” event. Thus we can receive the massage sent from the background (JSON) and renew it to the interface after analysis.
function init(){
    window.WebSocket = window.WebSocket || window.MozWebSocket;
    if (!window.WebSocket){
        alert("WebSocket not supported by this browser");
        return;
    }
    var websocket = new WebSocket("ws://127.0.0.1:8080/alarm/alarmServer");
    websocket.onopen = onopen;
    websocket.onclose = onclose;
    websocket.onmessage = onmessage;
    ...
}
function onmessage(evt){
    var data = evt.data;
    if(!data){
        return;
    }
    data = stringToJson(data);
    if(!data){
        return;
    }
    ...
}
function jsonToString(json){
    return JSON.stringify(json);
}
function stringToJson(str){
    try{
        str = str.replace(/\'/g, "\"");
        return JSON.parse(str);
    }catch(error){
        console.log(error);
    }
}
Websocket foreground-background process
 
Business Realization
Data Model
There are three business types to be used in this example: node, link and alarm. The background has provided implementation class and defined properties like name, position and line-width. In addition, the export of json data has also been available.
  interface IJSON{
      String toJSON();
  }
  class Data{
      String name;
      public Data(String name){
          this.name = name;
      }
  }
  class Node extends Data implements IJSON{
      public Node(String name, double x, double y){
          super(name);
          this.x = x;
          this.y = y;
      }
      double x, y;
      public String toJSON(){
          return "{\"name\":\"" + name + "\", \"x\":\"" + x + "\",\"y\":\"" + y + "\"}";
      }
  }
  class Link extends Data implements IJSON{
      public Link(String name, String from, String to, int width){
          super(name);
          this.from =from;
          this.to = to;
          this.width = width;
      }
      String from;
      String to;
      int width = 2;
      public String toJSON(){
          return "{\"name\":\"" + name + "\", \"from\":\"" + from + "\", \"to\":\"" + to + "\", \"width\":\"" + width + "\"}";
      }
  }
  class Alarm implements IJSON{
      public Alarm(String elementName, String alarmSeverity){
          this.alarmSeverity = alarmSeverity;
          this.elementName = elementName;
      }
      String alarmSeverity;
      String elementName;
@Override
public String toJSON() {
    return "{\"elementName\": \"" + elementName + "\", \"alarmSeverity\": \"" + alarmSeverity + "\"}";
}
  }
Three lists of data have been maintained in the background to save nodes, links and alarms respectively. Furthermore, it is easy to quickly find nodes since keys are named after node names in elementMap.
Map<String, Data> elementMap = new HashMap<String, AlarmServlet.Data>();
List<Node> nodes = new ArrayList<AlarmServlet.Node>();
List<Link> links = new ArrayList<AlarmServlet.Link>();
List<Alarm> alarms = new ArrayList<AlarmServlet.Alarm>();
To Initialize Data
In servlet construction, we have added some simulated data. When client-side makes connections (AlarmWebSocket#onOpen(Connection connection)), the background will send the information on nodes, links and alarms to the foreground in the form of JSON (sendMessage(this, “reload”, loadDatas());).

public AlarmServlet() {
    initDatas();
    ...
}

public void initDatas() {
    int i = 0;
    double cx = 350, cy = 230, a = 250, b = 180;
    nodes.add(new Node("center", cx, cy));
    double angle = 0, perAngle = 2 * Math.PI/10;
    while(i++ < 10){
        Node node = new Node("node_" + i, cx + a * Math.cos(angle), cy + b * Math.sin(angle));
        elementMap.put(node.name, node);
        nodes.add(node);
        angle += perAngle;
    }
    i = 0;
    while(i++ < 10){
        Link link = new Link("link_" + i, "center", "node_" + i, 1 + random.nextInt(10));
        elementMap.put(link.name, link);
        links.add(link);
    }
}

private String loadDatas(){
    StringBuffer result = new StringBuffer();
    result.append("{\"nodes\":");
    listToJSON(nodes, result);
    result.append(", \"links\":");
    listToJSON(links, result);
    result.append(", \"alarms\":");
    listToJSON(alarms, result);
    result.append("}");
    return result.toString();
}

   class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
           ...
    @Override
    public void onOpen(Connection connect) {
        this.connection = connect;
        clients.add(this);
        sendMessage(this, "reload", loadDatas());
    }
           ...
   }
To Show the Initial data in the Foreground
The initial data is sent to the client-side by the method sendMessage(…) from the background and received through the call-back function onmessage by the client-side. In this example, we use components in twaver html5 to show them. The use flow of Twaver components is as usual: first convert the data, that is, to switch JSON data to the element types of TWaver; then fill them to ElementBox; at last relate them to the network components. Here is the code:

public AlarmServlet() {
    initDatas();
    ...
}

public void initDatas() {
    int i = 0;
    double cx = 350, cy = 230, a = 250, b = 180;
    nodes.add(new Node("center", cx, cy));
    double angle = 0, perAngle = 2 * Math.PI/10;
    while(i++ < 10){
        Node node = new Node("node_" + i, cx + a * Math.cos(angle), cy + b * Math.sin(angle));
        elementMap.put(node.name, node);
        nodes.add(node);
        angle += perAngle;
    }
    i = 0;
    while(i++ < 10){
        Link link = new Link("link_" + i, "center", "node_" + i, 1 + random.nextInt(10));
        elementMap.put(link.name, link);
        links.add(link);
    }
}

private String loadDatas(){
    StringBuffer result = new StringBuffer();
    result.append("{\"nodes\":");
    listToJSON(nodes, result);
    result.append(", \"links\":");
    listToJSON(links, result);
    result.append(", \"alarms\":");
    listToJSON(alarms, result);
    result.append("}");
    return result.toString();
}

   class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
           ...
    @Override
    public void onOpen(Connection connect) {
        this.connection = connect;
        clients.add(this);
        sendMessage(this, "reload", loadDatas());
    }
           ...
   }
The effect

 
The Background Sends Alarms and the Foreground Instantly Renews
Add the code for sending alarms at the background: Here we have made a timer which generates a random alarm every two seconds or clear all the alarms and send the information to all client-sides.

The background code:
public AlarmServlet() {
    ...
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            if(random.nextInt(10) == 9){
                alarms.clear();
                sendMessage ("alarm.clear", "");
                return;
            }
            sendMessage("alarm.add", randomAlarm());
        }
    }, 0, 2000);
}
public void sendMessage(String action, String message) {
    for(AlarmWebSocket client : clients){
        sendMessage(client, action, message);
    }
}
private Random random = new Random();
private Data getRandomElement(){
    if(random.nextBoolean()){
        return nodes.get(random.nextInt(nodes.size()));
    }
    return links.get(random.nextInt(links.size()));
}
String[] alarmSeverities = new String[]{"Critical", "Major", "Minor", "Warning", "Indeterminate"};
private String randomAlarm(){
    Alarm alarm = new Alarm(getRandomElement().name, alarmSeverities[random.nextInt(alarmSeverities.length)]);
    alarms.add(alarm);
    return alarm.toJSON();
}
The foreground code:

There should be the corresponding operations when the client-side has received the messages: add the operations to messages like “alarm.clear” and “alarm.add”. In this way alarms can instantly refresh themselves.
function onmessage(evt){
    ...
    if(action == "alarm.clear"){
        box.getAlarmBox().clear();
        return;
    }
    data = data.data;
    if(!data){
        return;
    }
    ...
    if(action == "alarm.add"){
        newAlarm(data)
        return;
    }
    ...
}
Dragging Nodes at a Client-side to Synchronize to Others
At last, add drag synchronization and listen to the element-dragged events. At the end of dragging an element, the position of the node will be sent to the background.

Foreground code:
network.addInteractionListener(function(evt){
    var moveEnd = "MoveEnd";
    if(evt.kind.substr(-moveEnd.length) == moveEnd){
        var nodes = [];
        var selection = box.getSelectionModel().getSelection();
        selection.forEach(function(element){
            if(element instanceof twaver.Node){
                var xy = element.getCenterLocation();
                nodes.push({name: element.getName(), x: xy.x, y: xy.y});
            }
        });
        websocket.send(jsonToString({action: "node.move", data: nodes}));
    }
});
When the background has received the position of the node, the background data (the position of the node) will first be renewed, which will then be sent to other client-side, hence the simultaneous operation among client-sides.

Background code:
   class AlarmWebSocket implements org.eclipse.jetty.websocket.WebSocket.OnTextMessage
   {
           ...
    @Override
    public void onMessage(String message) {
        Object json = JSON.parse(message);
        if(!(json instanceof Map)){
            return;
        }
        Map map = (Map)json;
        Object action = map.get("action");
        Object data = map.get("data");
        if("node.move".equals(action)){
            if(!(data instanceof Object[])){
                return;
            }
            Object[] nodes = (Object[])data;
            for(Object nodeData : nodes){
                if(!(nodeData instanceof Map) || !((Map)nodeData).containsKey("name") || !((Map)nodeData).containsKey("x") || !((Map)nodeData).containsKey("y")){
                    continue;
                }
                String name = ((Map)nodeData).get("name").toString();
                Data element = elementMap.get(name);
                if(!(element instanceof Node)){
                    continue;
                }
                double x = Double.parseDouble(((Map)nodeData).get("x").toString());
                double y = Double.parseDouble(((Map)nodeData).get("y").toString());
                ((Node)element).x = x;
                ((Node)element).y = y;
            }

        }else{
            return;
        }
        for(AlarmWebSocket client : clients){
            if(this.equals(client)){
                continue;
            }
            sendMessage(client, null, message);
        }
    }
}
Structure: