Canva - magic mix drawings - Distributed Systems course 2014

961 views

Published on

Published in: Education, Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
961
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
5
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Canva - magic mix drawings - Distributed Systems course 2014

  1. 1. Technical University of Denmark 02220 Distributed Systems Canva - magic mix drawings s131181| Vlad Manea s131185| Laura-Ariadna Stroe s131044 | Nanna Gudrun Hjaltalin May 19, 2014 1
  2. 2. Table of contents pag. 6 ...... 1. Introduction pag. 6 ...... 2. Design pag. 7 .......... 2.1. Paradigms pag. 9 .............. 2.1.1. Event driven drawing pag. 9 .............. 2.1.2. Client - Server pag. 10 .............. 2.1.3. HTTP available server pag. 11 .............. 2.1.4. TCP event based socket reliable communication pag. 12 .............. 2.1.5. Branch driven repository pag. 13 .......... 2.2. Architecture pag. 13 .............. 2.2.1. System architecture pag. 15 .............. 2.2.2. Client side event based drawing system pag. 16 .............. 2.2.3. Full stack branching system pag. 16 .................. 2.2.3.1. Branch internal representation pag. 17 .................. 2.2.3.2. Overview of branch procedures pag. 18 .................. 2.2.3.3. Checking out a branch pag. 19 .................. 2.2.3.4. Saving a branch pag. 20 .................. 2.2.3.5. Merging branches pag. 21 .................. 2.2.3.6. Blocking and unblocking pag. 22 .............. 2.2.4. Full stack chat system pag. 23 .............. 2.2.5. Test scenarios pag. 23 .................. 2.2.5.1. Chat test scenario pag. 24 .................. 2.2.5.2. Drawing test scenario pag. 26 .................. 2.2.5.3. Branching test scenario pag. 29 .................. 2.2.5.4. Blocking test scenario pag. 31 ...... 3. Implementation 2
  3. 3. pag. 31 .......... 3.1. Technologies pag. 31 .............. 3.1.1. Node.js web framework pag. 32 .............. 3.1.2. Fabric.js canvas drawing framework pag. 32 .............. 3.1.3. Socket.IO web communication framework pag. 32 .............. 3.1.4. Jasmine.js test framework pag. 32 .......... 3.2. Workflow pag. 32 .............. 3.2.1. Timeline pag. 33 .............. 3.2.2. Tasks pag. 34 .............. 3.2.3. Repository pag. 34 .............. 3.2.4. Build pag. 34 .............. 3.2.5. Deployment pag. 35 .............. 3.2.6. Test pag. 35 .............. 3.2.7. Team pag. 35 ...... 4. Conclusions pag. 36 ...... List of references pag. 39 ...... Appendix A pag. 39 .......... A.1. Piping command line API modules pag. 39 .............. A.1.1. Strength pag. 40 .............. A.1.2. Weakness pag. 40 .............. A.1.3. Opportunity pag. 40 .............. A.1.4. Threat pag. 40 .......... A.2. Google Docs LaTeX addon pag. 40 .............. A.2.1. Strength pag. 40 .............. A.2.2. Weakness pag. 40 .............. A.2.3. Opportunity pag. 40 .............. A.2.4. Threat pag. 40 .......... A.3. Biker real time tracking 3
  4. 4. pag. 41 .............. A.3.1. Strength pag. 41 .............. A.3.2. Weakness pag. 41 .............. A.3.3. Opportunity pag. 41 .............. A.3.4. Threat pag. 41 ...... Appendix B pag. 41 .......... B1. Server side code package.json app.js chat.js config.js http.js io.js repository.js routes.js server.js pag. 54 .......... B.2. Client side code circle.js ellipse.js line.js polyline.js rectangle.js text.js triangle.js chat.js index.js modifier.js painter.js 4
  5. 5. repository.js socket.js ui.js style.css index.jade pag. 82 .......... B.3. Test code server-repository-spec.js server-chat-spec.js pag. 91 ...... Appendix C pag. 91 .......... C.1. A passing build pag. 94 .......... C.2. A failing build pag. 95 .......... C.3. The latest build and deploy runs 5
  6. 6. 1. Introduction The stated purpose of this project is to implement a self-chosen distributed system. Our aim was to find a real problem, so that its solution was complex enough to fit the project requirements for the course, while simple enough to permit implementation in the given time frame. We aimed for a project whose lifespan may have been prolonged beyond the course, and looked for a challenging and rewarding problem. We wished to learn many things while solving it, and enjoy our work. We made use of existing project requirements, proposed in [1] as a baseline. After brainstorming, we ended up with four ideas which met the objectives above: a collaborative drawing application, a modular framework for piping commands, a Google Docs LaTeX addon, and a real time biker tracker. We received approval for all of these, and then decided by vote on the drawing application. The other projects can be found in Appendix A. Collaborative drawing is a problem already solved by Google with Drawings [2] and Microsoft with Onenote [3]. Google also allows users to switch between different versions of these diagrams in time. This model works great for playful draw and simple diagrams made by small teams. But in a business environment, many times parts of larger teams propose different drawings of diagrams, or work on parts of the final drawing separately. The current solutions make “choosing the best and losing the rest” a hard burden on the teams. The strength of this project, aside from meeting our objectives above, consists of us having worked with web technologies before. We already have the basics, and can use these to learn more. Our work has the opportunity to jumpstart a useful and open-source modular product for enterprise and students, while still being suited for playful drawing. Two of us actually faced the problem at a different course. Our weakness is that not all team members have prior knowledge of versioning and branching, and none has experience in developing a system with these features. We were threaten by the project taking more than planned, which might have required us to prioritize the features and discard some. With these in mind, we decided to tackle the problem. We wish to empower users to not only work together, but also review and mix their work. To achieve this goal, we have developed canva [4]: a universally available collaborative drawing application, where groups of users can create multiple parts of the same drawing independently, and then discuss and combine them into magic. 2. Design Our system has been designed to implement the following features: 6
  7. 7. 1. A canvas on the client, which allows users to create custom shapes; each shape is defined by a module of its own 2. A branch based repository system, which allows users to share their drawings with others, start from scratch or improve existing versions, and combine their results by choosing only the relevant shapes 3. A system that allows users to work in real time on the same canvas, such that only one saves changes made by anybody else in that group 4. A messaging system, which allows user conversation during canvas drawing collaboration Each feature is described in its corresponding subsection. 2.1. Paradigms Our distributed system implements features based on a clear set of paradigms. These can be seen at work in Fig. 1. 7
  8. 8. Fig. 1. The paradigms in our distributed system at work. There are two clients who connect to the server to work on the same drawing. Both connect to fetch the page, and then to establish the socket handshake. Both clients concurrently check out a branch. Client 2 performs a change by adding a circle, commits the change locally, and then saves the branch onto the server. When client 1 checks out the branch again, it will obtain the latest version, and will be in sync with client 2. 8
  9. 9. 2.1.1. Event driven drawing The HTML5 canvas tag [5] does not provide implementation for each of the geometric shapes. This can be implemented, yet the canvas does not provide an out of box event system for shape management, which would allow moving, rotating and other operations. We drew our attention upon frameworks that could help us. We were in search of a drawing system which handled shape events on the canvas, and also had primitive shapes implemented. Our task would now be to implement the drawing of these primitive shapes by using the events we were provided. For instance, when drawing a circle, one first clicks on the canvas surface, and then moves the mouse onto a different position. In all this time, the user can see a preview circle on the canvas, such that the first click location is its center, and the current location defines the radius. When the user releases, the operation ends, and the circle is added to the canvas. This flow is depicted in Fig. 2. Fig. 2. Two events on the canvas define the center and the radius of a circle. These events can be seen as transitions in a deterministic finite state machine. We have created a DFSM for each shape type. 2.1.2. Client - Server We need to implement a system that manages the clients and their drawings, such that if a new change appears in one drawing, it will be made apparent to all involved clients, but not more. For example, if three teams of clients work on three separate drawings, each group would have a set of clients working on the same drawing. We want the server to be able to manage this group and other groups simultaneously and independently. This situation is depicted in Fig. 3. 9
  10. 10. Fig. 3. The server handles groups separately. This means that a client will be able to obtain the changes made by other clients in the same group, but not other clients and groups. The current browser based collaborative tools such as [2] implement the client - server paradigm. We did not see the advantages of implementing a more complicated, decentralized paradigm, such as peer to peer [6]. 2.1.3. HTTP available server Our clients are using the web for collaboration, so it naturally makes sense to use the web protocol and technology there is. This way, users will not have to install any runtime environment, such as JRE [7], before using our application. The HTTP protocol [8] is widely known and used by devices worldwide. From our project perspective, its main advantages are that it does require nothing more than a browser on the client, and that it can be used on both desktop and mobile devices out of the box, as in Fig. 4. In our project we use an initial HTTP request for fetching the initial page. This makes it a RIA [9], which allows easy user navigation: the person only has to use the client to browse there and start drawing. 10
  11. 11. Fig. 4. Sequence diagram of a communication. HTTP is a understood by many types of clients. This makes our application universally available. 2.1.4. TCP event based socket reliable communication The information related to the drawing has to be sent and received accurately. In order to achieve this, we need to ensure that information about the canvas is reliably sent and received. It is crucial that each client is notified on any change, and receives the correct change, if an active collaboration takes place. The reliability and message order keeping properties of TCP allow us to keep the branches on the client and server synchronized. On the other side, UDP is more suited for our simple message transmission, and we considered implementing it there. Message transmission reliability is only relevant in the context of an underlying reliable system. In this case, one which is either single threaded or allows locks, so that resources in the critical section are consistent at any time. The data structures for storing branches and users have to be either accessed by a single thread, or locked on write while reading. The implementation of this paradigm must therefore have one of these two properties. An example on how the sockets work is explained in Fig. 5. We assume that someone adds a line on the canvas during collaboration, and we want every client to be notified with the updated canvas, so they can continue drawing on top of it. A missing or misordered package would prevent the client from receiving the updated canvas, and would move it out of sync. In order to have client notification, we went to sockets, which work in TCP only. We are aware that chat messaging might also be done with UDP, for e.g., by using a technology such as [10]. Since the messages are small information, we preferred to use TCP and have a consistent codebase by using a TCP socket implementation. 11
  12. 12. Websockets is a set of protocols [11] which provides a full duplex reliable communication channels over a TCP connection [12], and is supported by multiple browsers such as Google Chrome, Internet Explorer, Firefox, Safari and Opera. It did not make sense to use pure HTTP for update notifications during drawing collaboration, because it had to be done in real time, and HTTP would still require the client to send requests to the server, by using AJAX [13] or similar. With websockets, a client can listen for changes coming from the server without polling, and it can pass messages in both directions at the same time (full duplex). [14] mentions a list of disadvantages of pure HTTP as opposed to websockets. Three out of the five signs described in [15] apply to our project, which indicates a true need for websockets. First, data must flow both ways and simultaneously. Second, the reference claims that websockets also provide a 3xlatency decrease and a 1000xspace decrease. Third, we need to avoid the server polling. Fig. 5. Sequence diagram of reliably emitting a change in the drawing, in this case the red line. One client emits the change to the server through the open socket, and then the server emits the change to all sockets. Now all clients have received the new line, without extra requests to the server. 2.1.5. Branch driven repository In order to support different versions, or parts, of the same drawing, one must use a system which is able to combine changes. The source control repositories [16], [17] [18] have solved this problem already, by allowing a single version path where clients sync, commit, update, and resolve. The Git system also resolved the problem of working on different branches, which would allow independent work and simpler merge different branches at the end. A flow example for Git is [19]. For example, we consider a scenario in which there are two teams with different versions of a solution to a common problem. The teams start from an older diagram, 12
  13. 13. and propose different improvements to it. Their proposals may have overlapping parts. After they finish working on their individual branches, they might want both solutions combined, meaning they want to keep the relevant parts from each of the solutions, and discard the unnecessary data from both of them. Therefore, it is necessary to have a branching system that gives the clients the possibility to also merge, in our case, their drawings. Such a process is explained in Fig. 6. Fig. 6. A branch driven repository. The circles represent new versions. There is a master branch, from which all branches are created - here depicted with the south-east arrows. From time to time, branch integrations are performed - here depicted with the north-east arrows. Some branch integrations might yield to conflicts which require resolution, e.g., by merging. 2.2. Architecture The client and server are implemented in a symmetrical way, such that module cohesion increases. The client - server communication takes part by using socket emit and socket-on events in the two sister socket modules, one on the server and one on the client. 2.2.1. System architecture The sister repository / chat modules on the server and on the client manage branches / users at the server and client level, respectively. The client contains additional modules for painting shapes, modifying shapes and interaction with the UI. The client also contains plug-in modules for various geometric shapes, which are registered to the painter in the index file. We have applied a lightweight version of the strategy pattern: when the user selects a shape name to be drawn, such as square, then the square module is used to update the canvas. The server additionally contains modules for HTTP communication and MVC route management - there is a single route, which corresponds to the single rich page. In Fig. 7 and Fig. 8 the client and server class diagrams can be seen. 13
  14. 14. Fig. 7. The client class diagram. Fig. 8. The server class diagram. 14
  15. 15. In the prototype stage, all code was implemented in a single class . However, after1 the prototype was functional, we separated the concerns in different modules. The repository and chat implementations on both client and server were separated from the socket managing class. The UI update functionality was also removed from the socket managing class. This allowed the socket class to only know about the socket instance, and the ui know about the jQuery objects. All responsibility regarding branches was passed to the repository class, and all responsibility regarding participants and messages was passed to the chat class. The modular structure allowed us to implement and use the drawing and branching features independently: we worked with figures in the canvas, and with plain numbers as objects in the branches. This has proven beneficial in two situations. The first was the integration of canvas objects into the branching system. Having all implementation done in these two separate modules, we could very easily integrate them. For instance, in order to commit the current state of the canvas, all we had to do was to to pass the output of the canvas, i.e., the result of the function canvas.toDatalessJSON()as the objectToCommitparameter of commitBranch, and follow the same pattern in other branching functions. The entire integration took around 15 minutes. The second was the ability to test the server repository and chat functionality independently, without requiring socket objects or mocks in the test harness. 2.2.2. Client side event based drawing system We have implemented modules for the following geometrical shapes: circle, ellipse, rectangle, triangle, line, polyline, and text box. The user can add one of these shapes to the canvas by clicking, dragging and releasing. The farther the drag, the larger the final object. A preview of the object is available when dragging. Each module implements a deterministic finite state machine. For instance, the circle module has three states. The first state corresponds to idle, and it is the start state. The second state corresponds to a circle whose center is known, but whose radius is unknown. The third state corresponds to a circle whose both center and radius are known. When the user selects the circle button and then clicks on the canvas, the DFSM goes from state 0 to state 1. At this state, the click location corresponds to the center. When the user releases the click, the DFSM goes from state 1 to state 2. The center and this location form a segment of length equal to the radius, and the circle is drawn onto the canvas. Then the DFSM is triggered to change state from 2 to 0, which is idle again. The DFSM manager automatically moves it from any state to state 0 when a different DFSM is initiated. The creation of a circle can also be seen in Fig. 2. 1 Our programming language of choice does not use classes, yet it is possible to simulate a light set of properties and patterns that apply to classes, such as encapsulation and dependency injection. 15
  16. 16. This modular structure proves beneficial, because all shapes are treated the same by the painter, and more modules can be easily added if registered in the index file. This will be helpful if the project lifespan is increased as open-source, and more developers adhere by writing shapes modules. The user can globally choose the border and fill color of all objects to be next created, and the border thickness. We have implemented a lightweight version of the flyweight design pattern for this: the color and thickness attributes are simply inserted into all objects. By using the text field, the user can add text, for e.g., if willing to describe something on the picture. We can see the drawing shapes existing in our app in Fig. 9. Fig. 9. The states of a canvas during drawing shape types, in order: a circle, a triangle, a line, an ellipse, a polyline, a text box. 2.2.3. Full stack branching system We want the our clients to be able to draw not only at the same time on the same branch, but also separately, with the possibility of combining the drawings when they want. Our branch system consists of a master branch, and a set of other branches, created from the master branch. When one branch is created from another, a version is created on the new branch, such that the object in that version is a clone of the object in the head of the branch from which the creation is done. After separating from a branch, our clients need to merge their work with any other branch. 2.2.3.1. Branch internal representation The branching system can be seen as a directed acyclic graph. The node with zero inner degree corresponds to a primordial empty version of the master branch. This node is created as part of the main branch when the branching system initializes. A path from the primordial master node to any node corresponds to the master branch, followed by the branch this version node belongs to. A version is a fixed state of the branch at a certain point in time. In our concrete case, states are in the universe of geometrical shapes placed on a canvas. This layout can be seen in Fig. 10. 16
  17. 17. Fig. 10. Branching system internal layout, situated in both client and server. A node in one branch may or may not point to a node in another branch, but it always points to a version node. The exception to this rule is the master primordial version node. 2.2.3.2. Overview of branch procedures The checkoutBranch(branchName) procedure allows the retrieval of the latest version of the branch with that name from the server. The master branch, along with its primordial empty version, got created when the server initializes, and can be checked out immediately: the user can either click the master button and then the checkout button in order to checkout from the master branch. After the client is checked out on the master branch, he can either start drawing in that branch, or create another branch from it. The master branch can not be deleted, so that there will always be a master branch. To start working on a branch, the client has to checkout that branch first, which can be done by pressing the button with the name of the branch and then pressing the checkout button. If the user does not want to draw on the master canvas, he has to create a new branch. At any moment in time, createBranch(branchName)creates a new branch with that name on the server, such that its primordial version points to the head of the currently checked out branch. In order to create a branch the user has to click the create button and then enter a name for the branch. At that point, a button with the name of the new branch will appear next to the master branch button, so that from this point on that branch can be checked out whenever necessary. This new branch will have all the information from the master branch up to the point when it was created. When a branch is created it is automatically added to the server. The user cannot create a branch if the underlying branch is not updated, i.e., it has changes not saved on the server. This situation is depicted in Fig. 11. 17
  18. 18. Fig. 11. Creating a branch. The dotted lines depict paths not saved on the server. In the left drawing, some changes were made to the underlying branch and were not saved; therefore, the create is aborted. In the right drawing, the branch is up to date with the server; therefore the create is completed. Every primordial node of the new branch points to the underlying node. To save the changes locally with commitBranch(objectToCommit), the client must use the commit button. This procedure saves the changes in the current branch, without sending them to the server. The client cannot commit on a branch if it is not the checked out branch. The saveBranch(branch)procedure adds a new branch if the branch did not exist, or attempts to save it on the server. If both existing server and incoming client branches have different head versions, the save is aborted and a conflict notification is sent back to the client. The mergeBranch(branchName) procedure merges locally the branch with the specified name into the current branch. The deleteBranch(branchName)inactivates the branch with the provided name. To delete a branch one must be checked out on that branch, and select the delete button. From that moment on, the branch becomes globally inactive. We decided to make it inactive rather than completely deleting it, because there might be other branches who point to one node in the branch to be deleted. Once a branch is inactive, it acts as if it does not exist anymore, meaning that one can no longer operate on that branch. 2.2.3.3. Checking out a branch In this section changes are considered committed changes, so they appear as head versions in the branches. On branch checking out on the client, there will be one of the following situations. If one has checked out on a different old branch before, and it made changes in that old branch, then the changes must be committed in the old branch before checking out a new branch. Otherwise, the changes are discarded. This situation can be seen in picture 1 of Fig. 12. 18
  19. 19. If the user wishes to check out a different branch, but there is no change in the old branch, there is no problem. The user can simply check out the new branch. The process can be seen in picture 2 of Fig. 12. If the user wishes to check out the same branch, i.e., fetch the latest updates from the server, there are four possible situations, as depicted in the pictures 3.1, 3.2, 3.3, 3.4 of Fig. 12. 1. None have changes. In this case, nothing happens. 2. The server branch has no changes, and the client has. In this case nothing happens; the client can continue working on his branch. 3. The client branch has no changes, but the server has. In this case, the client branch will be updated to the final version of the server branch. 4. Both have changes, and a conflict appears. To solve this, the user is asked to merge the branches. The merge process is explained in the following pages. Fig. 12. Checking out. A dashed line corresponds to a committed, but not saved change. The red and magenta versions correspond to two client branches. The green versions correspond to the server branch. 2.2.3.4. Saving a branch Unlike the commit procedure, the save procedure is made on both client and server. The user can save a branch by pressing the save button while being checked out to that branch. There are two situations. 19
  20. 20. If the branch was not saved before, then it is now created and added to the server. If the branch was saved before, then it follows that it already exists on the server. In this case, there are four possible scenarios. If neither the client, nor the server made any changes, then they have the same versions, and their head versions are the same. In this case, nothing happens, since there is no need for any update or merge. If the client made changes to the branch, while the server did not, then the client is ahead of the server, and his changes are to be added to the server. After this procedure, both client and server have the same head version. If the client made no changes, but the server did, then the server is ahead of the client and nothing happens. To get the updated version of the branch, the client will have to check out the branch. If both server and client made changes, there is a conflict. There are three important values, which we denote conflicting values. The first is the last common version of the client and the server, i.e., the latest version where they have the same information. The second is the last version in the client branch. The third is the last version in the server branch. The save aborts, and the server requires the client to check out to get the last version. At check out, the client will enter the 3.4 case depicted in Fig. 12. Here the client is required to merge the conflicting branches. It is easy to observe these four cases are similar to the four same branch check out cases. The similarity is further explained in the following section. 2.2.3.5. Merging branches The merge feature is used in two situations. First, at check out, upon a failed save - if the user decides to check out. Second, when the user merges the heads of two two different branches locally. In this scenario, a user is faced with the situation in which both client and server made changes, and was requested to merge. At this point, the user will see four canvases instead of one. Merging in this case means moving objects from one canvas to another, or to the recycle bin. These actions can be done by clicking on each object in particular. If an object from the left or the right canvas is clicked, then that object will move to the middle canvas, which represents how the merged version in the branch will look after the process. If one clicks on an object in the middle canvas, and that object is part of the common part of the two versions, then that object will be sent to the recycle bin. If the object in the middle, which was received by one of the left or right versions, is being clicked, then it is sent back to the canvas it belonged to. Things sent to the recycle bin can be restored by being clicked. This interaction ensures complete undo capability. A scenario in which merge is required can be seen in Fig. 13. The canvas to the left represents the changes committed by the client in this branch, and the canvas to 20
  21. 21. the right represents the changes saved on the server in this branch, by other clients. The middle canvas initially depicts the last version in which both branches were equal. Concretely, they both had the square, but the client the committed a circle, while on the server there was a triangle added. In the image, the rectangle which is in both versions, noted left and right, is faded out in gray and not selectable. This is because clicking moves them to the middle canvas, but it already exists there. This will only duplicate the object, putting them on top of each other. After all the desired shapes are placed in the middle canvas, the user can click the ok button to merge. The user can also cancel at any point, which also aborts the underlying check out, if any. Fig. 13. Merging branches. There are four canvases: your changes on the left, their changes on the right, the baseline (and also final version once the ok button is clicked) in the center, and the recycle bin below 2.2.3.6. Blocking and unblocking We wish to allow the branching system to help users draw in two ways: first, separately, so that any of their saved changes does not affect other users checked out on that branch; and second, such that they can collaborate on drawing in real time. In order to achieve these two separate objectives, we implemented a system that blocks the save operation for all except one client, and allows for client notification on any change other client is doing in that branch. This is where we harness the power of sockets the most! At any time when there is no such block, a client must check out the branch in order to receive the updated version of that branch. To let clients draw on the same canvas in real time, we created a blocked(data) function. When a client blocks, it is given a unique token which validates its control over saving that branch. A client can only block a branch if the branch is active, which means it was not deleted, and if it is not already blocked by another client. When a client blocked a branch, any other client can check out that branch and make changes. The change is sent to the server, which notifies all participants that a change has been made. Only the client having the token automatically commits and 21
  22. 22. saves the change in the blocked branch. If another client who checked out on that branch tries to save, they will receive an error message, because in order to save, the client must have the token that was given at blocking. When the client in control of that branch wants to release control, it will turn the token back in, and unblock the branch, making it available for blocking by others, as depicted in Fig. 14. If the block lasted for more than a specified period of time, a new block attempt will succeed. This is useful for the case when the client who blocked loses connection and cannot unblock anymore. Fig. 14. A scenario for the block and unblock. There are two clients. Assuming the second and third blocks are issued before the timeout, the second client can only block the branch successfully after the first client unblocked that branch. 2.2.4. Full stack chat system In order to allow users to communicate without leaving the drawing application, we have developed a simple chat system on top of websockets. When a user connects to our application and also when sends a message to the server, all other clients are notified a new participant or message appeared, and their clients update the user interfaces with the new data. We have used the tutorial [20] for learning how to work with websockets and get our chat up and running. In our chat each participant can register with his or her name, and start chatting right away. If the user does not register with a name, he will appear as anonymous. The participant is automatically registered as anonymous when he enters the website. Even if two participants have the same name, there will be no conflict when 22
  23. 23. sending the information, because one object is identified not only by the name of its author, but also by an the client socket id. 2.2.5. Test scenarios We have split the testing of our application into automatic and manual. We have tested automatically all responses the repository and the chat provide for various operations. For each operation, we tested both valid and invalid input. This provides us trust that the server performs correctly regardless of client message structure . A sample test case can be seen in Fig. 15.2 it("should trigger ok if token is valid", function() { var branchName = "existingBranch"; var participantId = 1; repo.saveBranch( {branch: {name: branchName, nodes: [], active: true}}); var blockResponse = repo.block( {participant: {id: participantId}, branch: {name: branchName}}); var result = repo.unblock({participant: {id: participantId}, branch: {name: branchName}, token: blockResponse.token}); expect(result.status).toEqual('OK'); expect(result.branch.name).toEqual(branchName); expect(repo.isBlocked(branchName)).not.toBeTruthy(); }); Fig. 15. Testing the invalid token. On the client we could not make use of the server testing framework, due to problems integrating the vanilla JavaScript files into the Node tests. We tried to no avail to make use of the advice at [21]. We wanted to make sure our features, the chat, the drawing, the collaborative drawing and the branching system would work before we move on to the next step. Therefore we tested manually our application at every iteration. In the last iteration of our project, we created and ran the following manual test scenarios, covering the full stack. 2.2.5.1. Chat test scenario Performed action Expected result connect with a client there is no canvas in the user interface 2 We are aware of the fact that we did not perform security testing beyond the protocols we created for branching and blocking branches. This means that a missing token will be caught, while a spoofing attack on the token passing will not. 23
  24. 24. connect with the first client its name appears as anonymous among the participants in the user interface; its name is followed by the “you” mark write a message and click share the message appears in the user interface connect with the second client there is no message visible; its name appears among the participants, but the other appears too; its name is followed by the “you” mark write a message and click share the message appears on both clients. change name of the first client, and focus out of the field without sending any message both clients get the updated name This scenario run did not uncover issues. 2.2.5.2. Drawing test scenario Performed action Expected result connect with a client there is no canvas in the user interface click the master (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve the canvas appears in the user interface click the circle button click the canvas, keep the button clicked, and move the mouse a preview circle is dynamically generated while moving the mouse release the click button of the mouse, while being still in the canvas a circle is created, instead of the last preview circle click the ellipse button click the canvas, keep the button clicked, and move the mouse a preview ellipse is dynamically generated while moving the mouse release the click button of the mouse, while being still in the canvas an ellipse is created, instead of the last preview ellipse click stroke color, select a color, click outside of canvas or picker to select the color 24
  25. 25. click the rectangle button click the canvas, keep the button clicked, and move the mouse a preview rectangle is dynamically generated while moving the mouse release the click button of the mouse, while being still in the canvas a rectangle is created with the stroke color, instead of the last preview rectangle move the stroke width slider click the triangle button click the canvas, keep the button clicked, and move the mouse a preview triangle is dynamically generated while moving the mouse release the click button of the mouse, while being still in the canvas a triangle is created with the stroke color and width, instead of the last preview triangle click the text button click the canvas a popup asking for the text appears introduce the text and click ok the text appeared, such that its top left corner is the point where clicked This scenario run did not uncover issues. 2.2.5.3. Branching test scenario Performed action Expected result connect with the first client there is no canvas in the user interface click the master (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve a canvas appears in the upper part of the user interface; assume the canvas has no content draw a shape on the canvas the shape appears on the canvas click the commit button a popup appears to notify that the change has been committed (locally, only on the client) click the save button a popup appears to notify that the branch master has been saved connect with a second client there is no canvas in the user interface 25
  26. 26. click the master (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve a canvas appears in the upper part of the user interface; the canvas has the content drawn by the first client draw a shape on the canvas the shape appears on the canvas click the commit button a popup appears to notify that the change has been committed (locally, only on the client) click the ok button to approve click the save button a popup appears to notify that the branch master has been saved click the ok button to approve switch to the first client nothing changed in the canvas for this client; there is only the shape drawn previously by this client draw a shape on the canvas the shape appears on the canvas click the commit button a popup appears to notify that the change has been committed (locally, only on the client) click the ok button to approve click the save button a popup appears to notify that the save was not possible because there is another save on this branch; the user is asked to check out the branch to get the latest changes click the ok button to approve click the master (branch) button click the check out button the merge screen appears; the shape drawn initially is grey; the shape by this client is in the left; the shape by the other client is in the right; the shape drawn initially, which was saved on the server, appears in the middle canvas as well; the recycle bin canvas is empty click the shape from your changes (the left canvas) the shape moves from left to middle canvas click the shape from their changes (the right canvas) the shape moves from right to middle canvas 26
  27. 27. click the shape that was initially in the middle canvas the shape moves from middle canvas to the recycle bin canvas click the shape that was initially in the left canvas the shape moves back from middle to left canvas click the ok button to approve a popup appears to notify that the branch has been resolved; the canvas did not change click the check out button a popup appears to notify that there was no change from the server; the canvas is updated so that it looks exactly like the final state of the middle canvas (having only the shape that was in the right) click the ok button to approve click save a popup appears to notify that the branch master was saved; this means that the conflict was resolved from the server perspective too click the ok button to approve switch to the second client its canvas contains the two shapes as before click the check out button a popup appears to notify that the branch has been updated click the ok button to approve the branch with only one objects, as saved by the first client after merging, is shown click the create branch button a popup appears to request a name, say A write a different name and click the ok button to approve the branch button A appeared close to the master (branch) button click the create branch button a popup appears to request a name, say B write a different name and click the ok button to approve the branch button B appeared close to the master (branch) button click the A (branch) button click the check out button a popup appears to notify that the branch has been checked out click the ok button to approve no change in the canvas as compared with master draw a shape in the canvas the shape appears on the canvas click the commit button a popup appears to notify that the change has been committed (locally, only on the client) 27
  28. 28. click the B (branch) button click the check out button a popup appears to notify that the branch has been checked out click the ok button to approve no change in the canvas as compared with master draw a shape in the canvas the shape appears on the canvas click the commit button a popup appears to notify that the change has been committed (locally, only on the client) click the ok button to approve click the A (branch) button click the merge button the user interface enters the merge screen; the three shapes look according to their origin: one active and one gray (inactive) on the left, one common in the middle, one active and one gray (inactive) on the right click on the left active shape shape is moved from left to middle canvas click on the right active shape shape is moved from right to middle canvas click the ok button to approve a popup appears to notify that the branch merge has been resolved click the ok button to approve click the save button a popup appears to notify that the branch B was saved click the ok button to approve click the B (branch) button click the check out button the resolved branch appears switch to the first client the canvas is unchanged, and the B (branch) button appears click the B (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve the canvas is populated as the B canvas was saved click the A (branch) button 28
  29. 29. click the delete button a popup appears to notify the delete click the ok button to approve the B (branch) button disappeared switch to the second client the B (branch) button disappeared This scenario run uncovered two issues in notifying the client on delete. They have been found in the server logs and fixed immediately. 2.2.5.4. Blocking test scenario Performed action Expected result connect with a first client there is no canvas in the user interface click the master (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve a canvas appears in the upper part of the user interface; assume the canvas has no content click on the block button a popup appears to notify that the branch master was blocked click the ok button to approve connect with a second client there is no canvas in the user interface click the master (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve a canvas appears in the upper part of the user interface; assume the canvas has no content draw a shape on the canvas the shape is drawn on the canvas switch to the first client the shape can be seen as updated by the second client; this happens because now the branch is in blocked save state switch to the second client click the save button a popup appears to notify that the branch could not be saved; this is because the branch has been blocked by another client who also got a token to unblock click the ok button to approve 29
  30. 30. click the unblock button a popup appears to notify that the branch cannot be unblocked; this is because the branch has been blocked by another client who also got a token to unblock click the ok button to approve click the block button a popup appears to notify that the branch cannot be blocked; this is because the branch has been blocked by another client who also got a token to unblock click the ok button to approve switch to the first client click the unblock button a popup appears to notify that the branch master was unblocked click the ok button to approve draw a shape on the canvas the shape is drawn on the canvas switch to the second client the canvas does not include the last drawn shape; this is because the branch is not in blocked state anymore draw a shape on the canvas the shape is drawn on the canvas click the commit button a popup appears to notify that the change has been committed (locally, only on the client) click the ok button to approve click the save button a popup appears to notify that the branch master was saved click the ok button to approve switch to the first client click the master (branch) button click the check out button a popup appears to notify that the branch has been fetched click the ok button to approve a canvas appears as it was saved by the other client This scenario run did not uncover issues. 30
  31. 31. We have performed brief checks on the application to see that the features are functional and work as expected on Chrome 34, Internet Explorer 11 and Firefox 29. 3. Implementation We wished to find technologies that fit the solution to the distributed branch based collaborative canvas problem, allowed us to use our prior programming knowledge, learn new concepts along the way, and make the application programming language consistent. We therefore avoided a multitude of unrelated technologies, and went with only one language for the entire stack. This allowed us learn how to design, develop and test one feature, and then apply what we learnt on other features. The source code can be seen in Appendix B. 3.1. Technologies In the context of universally available application, and by using the prior knowledge of one team member, we went with the safe and reliable Node.js stack. Throughout the project, we were able to make proper use of many node modules and gather them into a coherent and modular product. 3.1.1. Node.js web framework Node.js [22] is a software platform on top of JavaScript [23]. This platform was the choice for our project, due to its benefits as compared with different other platforms [24] [25], in the context of our project. We chose Node.js for a number of reasons. Node.js implementation on the server is done in JavaScript, which is the de facto language for client side web applications. This way we were able to use a singular programming language, and apply our skills for a module while developing others. One of our group members already implemented several applications and has knowledge about Node.js, and this would make it easy for the rest of the group members to ramp up, towards a working level of understanding. Node.js has active communities [22], resourceful tutorials and forums for discussion . In the same way as the other platforms considered, it is easy to install multiple modules [26] and use them right away. For our project in particular, we used jade [28] for writing easier client HTML code, jQuery [28] for HTML element management, underscore [29] for faster search and retrieval in lists, and socket [link] for client and server communication. Node.js has support for testing, via the jasmine [jasmine] module. A Node.js applications can also be easily built and tested continuously with Codeship [30], and deployed on Heroku [link]. All these facilities are provided free of charge. 31
  32. 32. 3.1.2. Fabric.js canvas drawing framework Fabric.js [31] is a JavaScript library which allows for easy creation and management of objects drawn onto a canvas. It includes a few primitives, such as lines, circles, rectangles and text boxes, which we implemented as modules in our project. We used the provided event system, in order to construct the DFSM for each element. The canvas comes decorated with events for object selection, movement, and rotation, which would not come out of the box if we used the bare canvas HTML5 tag. 3.1.3. Socket.IO web communication framework For our project, we needed to maintain persistent connection between the server and the clients, that is able to send updates in real time, and be able to support many clients at once. Socket.io [32] is a JavaScript library for realtime web applications. It can be used both client side in the browser, and server side in the node server. Socket.IO uses the websocket protocol. According to [32], Socket.IO does more than websocket, even if websocket is selected as the transport and the user is browsing the website with a modern browser. Certain features like heartbeats, timeouts and disconnection support are vital to real time applications, but are not provided by the websocket API out of the box. According to [33], the Socket.IO stack runs on a single thread, which is also the Node.js thread. This allows requests to be handled one by one, and discards the risk of critical section inconsistencies when reading and writing the data structures containing the participants and the branches on the server. 3.1.4. Jasmine.js test framework Jasmine.js is a Node.js test framework for JavaScript. We decided to use Jasmine.js to test our program because tests are written in JavaScript, the language of use in our project, can be easily integrated into the continuous integration system. We made use of tutorials [34], [35] and [36]. One of the group members had prior knowledge of the framework and ramped up the team in using it. 3.2. Workflow We have implemented this project in iterations. One iteration over the project would have either added a new module, or would have integrated multiple modules. 3.2.1. Timeline Before iterating the project, we ramped up and setup the repository, checked that 32
  33. 33. everybody can contribute to the project and everybody was provided the resources to learn JavaScript, Node, Fabric and the other technologies we decided to use. We used an online space to share written and video resources, and also set meetings. Our first iteration over the project consisted of developing a simple and working chat system as a prototype, using the technologies at hand. At the end of the first iteration, we setup the build and deploy systems. Our second iteration consisted of developing an offline canvas, where a single user could draw shapes on the canvas. During this iteration, we distributed our work for developing modules. In our third iteration, we integrated the offline canvas into a system similar to the chat system, so that multiple clients could collaboratively draw shapes. In the fourth iteration, we created a branch system independent from the canvas and chat, by using our knowledge from the chat. The branch system used bare numbers as objects, instead of real canvas states. In the fifth iteration, we integrated the online canvas with the branch system, by using the output of one as input to the other. We also wrote automated tests for the server at this point. In the sixth iteration, we split the blocked and unblocked states of branches, in order to allow both real time collaboration on drawing, i.e, fetching all changes from any client on that branch and update all clients, and classic repository workflow for the collaboration on drawing, i.e., requiring the client to check out a branch in order to get the latest changes. At the end of the iteration, we performed the manual scenario tests presented above. 3.2.2. Tasks We split our work into features and tasks. We used Trello [37] for organizing the tasks in the project, and assigning tasks to team members. It helped us always have an overview over the whole project, what was already done and what was still to be done, and who was where at any point in time. We created five columns: Features, To Do, Doing, Ready and Done. First, every feature is in the Feature column. Features are broken down to one or more tasks put in the To Do column. When one or more members starts solving a task, he or she moves that specific task to the Doing column; when it is done, it will be moved to the Ready column. At that point, the task is functional and working on his or her feature branch, pending merge with the master branch. When the task is merged onto the master, it is moved from Ready to Done. For a better view of our effort areas, we created labels for tasks: development, testing, integration, documentation, build and management. The Trello up to date board can be found at [38]. 33
  34. 34. 3.2.3. Repository We needed a software for collaborative programming. It was important for each of us to not interfere with the work of others, so we searched for a repository to work in our own branches. We therefore decided to use a Git-based repository. For our project we needed a source code hosting site that would allow us to create and work in a private repository for free. We also wanted to be able to review or our code at anytime. We considered a Git-based repository on GitHub [39] at first, but unfortunately on GitHub you cannot have a private repository for free. We drawn our attention on BitBucket [40], which also hosts Git-based repositories. Our repository can be found at [41]. Not only it permits private free repositories, but also has easy features in the UI. One of these is the pull request, which is used as a tool for code review. We used this feature when the members could not meet in person. However, on the general case, the team was cohesive enough to not have the problem of introducing faulty code. Partly, this was possible due to the continuous integration system. 3.2.4. Build We decided to try out a system in which we check in the code, and it appears in “production”. This way we could quickly see how it looks, feels and runs on the web, instead of our local repositories. Furthermore, it would help us run the tests automatically with every check in. For this part, one team member had prior knowledge of such products, while never setup a build and deploy system. We found a very good build system Codeship [30], which is a continuous integration and continuous deployment platform. It integrates BitBucket repositories and Heroku deployed sites, and has easy support for Node.js. On the testing part, it allowed any Node.j framework, in particular it worked with our choice Jasmine. In order to run our tests on the platform, we made use of the tutorial at [42]. The continuous integration system had one drawback: it only had a limited number of builds in the free version. For this reason, we had to make sure what we pushed onto the repository was functional. We checked if the program was functional and committed locally into our repositories, and only merged to master or pushed to the server when we were feature complete. 3.2.5. Deployment We used Heroku for hosting our website at [4]. Heroku is a cloud application platform supporting several languages, including JavaScript, and the Node.js framework. We found the logging feature [43] particularly useful one time, when the live website crashed during testing. 34
  35. 35. 3.2.6. Test The Jasmine framework is primarily a Node.js module, which made it a simple step in the Codeship set of commands to build and deploy. Complete build and deployment tasks, along with test results, can be found in Appendix C. 3.2.7. Team At the beginning of the project, all three members contributed to idea selection, repository setup and research, on both web framework and drawing libraries. In the initial stages of the project, we worked together on getting a chat prototype up and running. Here Vlad implemented the code, and all three members did test. We also collaborated all together for the first drawing module, which was a circle. Then we split work: Laura and Vlad worked on the rectangle, Laura worked with Nanna on the text and line modules, Nanna took the ellipse and Laura took the triangle. This feature was the second most complex feature in the project. A side task to improve the canvas, which consisted of adding color and weight for the stroke, and also integration, was done by Vlad and Laura. The integration of all drawing modules onto the canvas and the canvas server integration was implemented by Vlad and tested by all the team during development. The most complex feature of the project, which consisted of the branching system, was designed by Vlad with help from Laura, and implemented by both through pair programming. The merge operations and the block token system were designed by both and implemented by Vlad. All team members tested the system on Heroku during the entire scope of the project. The testing tasks, except continuous testing, were split as follows: Vlad and Laura worked on the automated tests and designed the scenario tests. The build and deployment system was setup by Vlad. We decided to write the documentation iteratively, such that members write chapters that are reviewed by other members. For the documentation editing, Nanna had a key role in technology chapters, and all team members contributed to the chapters on architecture and workflow. We wrote the introduction and the conclusions together. 4. Conclusions We have found a challenging and rewarding problem. We are happy to claim we provide a viable solution for the need of collaborative drawing, which is useful for many people. Our application might prove useful for enterprise and students, while still being suited for playful drawing. This is why we thought it as a great headstart for an open 35
  36. 36. source project. We have a working repository, development, testing and deployment system around it, which provide the infrastructure for a larger project. The project is currently universally available. Separating the workflow into multiple modules and tasks helped us not only plan our work efficiently, but also have permanent knowledge of our status. We consider we had a good approach in slicing the product into iterations. This allowed us to get some features almost for free. While not all team members have prior knowledge of versioning and branching, and none has experience in developing a system with these features, we completed a functional application by learning new things everyday. We were able to slice the features such that we had a working product early. Our initial plan was to develop a peer to peer and cloud backed application, yet we realized that peer to peer does not solve this problem, and we did not have the time resources to save data in the cloud. The future work would be focused on this, and also on highly improved user experience and security. The team members had the opportunity to improve their skills in designing and implementing a distributed system, and get acquainted with a web communication framework. All team members see this collaboration as a positive experience. There were no dull moments, the project was interactive, and we enjoyed working together. List of references 1. Sabin Corneliu Buraga. Human Computer Interaction project proposal. Alexandru Ioan Cuza University of Iasi. 2014 http://profs.info.uaic.ro/~busaco/teach/courses/hci/projects/index.html 2. Google. Drawings. 2014 https://docs.google.com/drawings/ 3. Microsoft. OneNote. 2014 http://www.onenote.com/ 4. Canva. Our project deployed on Heroku. 2014 http://canva.herokuapp.com/ 5. Mark Pilgrim. Dive into HTML5. Let’s call it a draw(ing) interface. 2011 http://diveintohtml5.info/canvas.html 6. James Cope. QuickStudy: Peer-to-Peer network. 2002 http://www.computerworld.com/s/article/69883/Peer_to_Peer_Network 7. Java Programming Environment and the Java Runtime Environment. 2010 http://docs.oracle.com/cd/E19455-01/806-3461/6jck06gqd/index.html 36
  37. 37. 8. W3C. HTTP - Hypertext Transfer Protocol. 2003 http://www.w3.org/Protocols/ 9. QuinStreet Webopedia. Rich Internet Application. 2014 http://www.webopedia.com/TERM/R/Rich_Internet_Application.html 10.Joyent, Inc.UDP / Datagram Sockets Node.js v0.10.28 Manual & Documentation. 2014 http://nodejs.org/api/dgram.html 11.Kaazing Corporation. About HTML5 WebSockets. 2013 http://www.websocket.org/aboutwebsocket.html 12.Information Sciences Institute. University of Southern California. Transmission Control Protocol. RFC 793. 1981 http://www.ietf.org/rfc/rfc793.txt 13.Mozilla Developer Network and individual contributors. Ajax. 2014 https://developer.mozilla.org/en/docs/AJAX 14.StackOverflow. Do HTML WebSockets maintain an open connection for each client? Does this scale?. 2013 http://stackoverflow.com/questions/4852702/do-html-websockets-maintain-a n-open-connection-for-each-client-does-this-scale 15.Peter Lubbers. Five Signs You Need HTML5 WebSockets. 2008 http://peterlubbers.sys-con.com/node/1551694/mobile 16.Software Freedom Conservancy. git. Retrieved 2014 http://git-scm.com/ 17.Apache. Subversion. 2011 http://subversion.apache.org/ 18.Mercurial. Retrieved 2014 http://mercurial.selenic.com/ 19.Atlassian. Feature Branch Workflow. Retrieved 2014 https://www.atlassian.com/git/workflows#!workflow-feature-branch 20.William Mora. Node.js Tutorial - Building a Chatroom with Express.js + Socket.IO. 2013 http://www.williammora.com/2013/03/nodejs-tutorial-building-chatroom-with .html 21.StackOverflow. Load “Vanilla” Javascript Libraries into Node.js. 2011 http://stackoverflow.com/questions/5171213/load-vanilla-javascript-libraries-i nto-node-js 37
  38. 38. 22.Joyent, Inc. node.js. Retrieved 2014 http://nodejs.org/about/ 23.Mozilla Developer Network and individual contributors. JavaScript. 2014 https://developer.mozilla.org/en/docs/Web/JavaScript 24.Microsoft. ASP.net MVC Overview. 2014 http://www.asp.net/mvc/tutorials/older-versions/overview/asp-net-mvc-overv iew 25.David Heinemeier Hansson. Ruby on Rails. Retrieved 2014 http://rubyonrails.org 26.Joyent, Inc. Node packaged modules. Retrieved 201 https://www.npmjs.org 27.Jade language. Retrieved 2014 http://jade-lang.com/ 28.The JQuery foundation. jQuery. 2014 http://jquery.com/ 29.Jeremy Ashkenas. underscore. GitHub. 2014 http://underscorejs.org/ 30.Codeship. Retrieved 2014 https://www.codeship.io/ 31.Juriy Zaytsev, Stefan Kienzle. Fabric.js. Retrieved 2014 http://fabricjs.com/ 32.Guillermo Rauch. Socket.IO. Automattic. 2014 http://socket.io/ 33.Codevate Limited. Developing a scalable real-time desktop or mobile application with Socket.IO, Redis and HAProxy. 2013 http://www.codevate.com/blog/9-developing-a-scalable-real-time-desktop-or- mobile-application-with-socketio-redis-and-haproxy 34.Rob Gravelle. Testing JavaScript Using the Jasmine Framework. 2014 http://www.htmlgoodies.com/beyond/javascript/testing-javascript-using-the-j asmine-framework.html 35.Clemens Helm. Testing Tuesday #19: Testing node.js applications with Jasmine. Video resource. Codeship. 2014 http://blog.codeship.io/2013/08/20/testing-tuesday-19-how-to-test-node-js-a pplications-with-jasmine.html 36.Clemens Helm. Testing Tuesday #16: JavaScript Testing with Jasmine. Video resource. Codeship. 2014 38
  39. 39. http://blog.codeship.io/2013/07/30/testing-tuesday-16-javascript-testing-with -jasmine.html 37.Fog Creek Software, Inc. Trello. 2014 https://trello.com/ 38.Canva Trello. Our task board. 2014 https://trello.com/b/BEYARHxo/canvadtu 39.GitHub, Inc. GitHub. 2014 https://github.com/ 40.Atlassian. Bitbucket. 2014 https://bitbucket.org/ 41.Canva Bitbucket. Our repository. 2014 https://bitbucket.org/vladmanea/ds-project 42.Clemens Helm. Testing Tuesday #20: Continuous Deployment for node.js applications. Video resource. Codeship. 2014 http://blog.codeship.io/2013/08/27/testing-tuesday-20-continuous-deployme nt-for-node-js-applications.html 43.Heroku. Logging. Retrieved 2014 https://devcenter.heroku.com/articles/logging Appendix A The other three proposed ideas are described below: A.1. Piping command line API modules It takes days for developers to mash up various API results from the social web, and all it comes up with is IO and a bit of processing. We wanted to develop a framework for developers to mash-up API results quicker, by using command line modules. Developers can pipe command lines which retrieve and forward API data. For example, piping the commands against the Twitter and Facebook modules, such as twitter get last 10 tweets @vladmanea {title, content, time} | facebook post all messages vlad.manea {title, body:content, moment:utc(time)}would fetch the last 10 tweets from Vlad’s Twitter account, and post each tweet as a message on Vlad’s Facebook account. The accolades represent the attribute mappings, and the last attribute is a call of an arbitrarily implemented function inside the Facebook module on the output of the Twitter module. Based on this paradigm, should it prove beneficial, API modules can be implemented and perfected by the community, just like Passport did with authentication providers. 39
  40. 40. A.1.1. Strength We can implement a powerful concept with cloud computation. A.1.2. Weakness While developing some sample modules, we might stumble upon programming limits. A.1.3. Opportunity Make web developer life easy, and build up a community around our product. A.1.4. Threat The project might be related to distributed systems on a higher layer. A.2. Google Docs LaTeX addon Currently we have not seen a coherent, free collaborative editor for academic documents. We wanted to develop an addon to be integrated with Google docs, which would allow constructing LaTeX framework formulae. The Google Docs addon technology is a new technology. For instance, a part of the document written in LaTeX is collaboratively edited by more user, and then selected. A formula picture is generated on the fly, and can be inserted in place. A nice to have would be making use of the Google Docs version control to ensure undo back in LaTeX. A.2.1. Strength We can make use of a compiler in the Google Docs trustworthy environment. A.2.2. Weakness In this time frame not all formulae or functionality might be implemented. A.2.3. Opportunity Leverage the power of Google Docs for a more collaborative research world. A.2.4. Threat The project might relate to distributed systems on a higher layer. A.3. Biker real time tracking Going on a bike ride takes a lot of post-processing time. Current applications focus on the competition and metrics after the fact only, which makes it a rather static 40
  41. 41. process. We wish to develop a simple application for peer-to-peer finding riding friends nearby, and fast and furious races with them. Based on the location of your fellow riders (e.g., your Google+ contacts), we tell you how you stack up against them in a race all along Strandvejen. A.3.1. Strength We can make use of geolocation to make it a fun experience. A.3.2. Weakness May take more time to prove the concept feasible in practice. A.3.3. Opportunity Ride healthy, make it a fun experience, and perhaps improve some records? A.3.4. Threat The project might relate to distributed systems on a higher layer. Appendix B B1. Server side code package.json { "name": "Drawr", "version": "0.0.1", "description": "Drawr", "main": "server.js", "author": "Vlad Manea, Laura Stroe, Nanna Hjaltalin", "dependencies": { "socket.io": "0.9.16", "express": "3.4.8", "jade": "1.3.1", "jquery": "1.11.0", "stylus": "0.43.1", "underscore": "1.6.0" }, "engines": { "node": "0.10.x", "npm": "1.2.x" }, "scripts": { "start": "node server.js" } } 41
  42. 42. app.js var express = require('express'); var config = require('./config'); var routes = require('./routes'); var app = express(); if ('development' == app.get('env')) { config.configDev(app); } if ('production' == app.get('env')) { config.configProd(app); } module.exports = app; chat.js var _ = require('underscore'); function Chat() { this.participants = []; this.addParticipant = function(data) { this.participants.push({id: data.id, name: data.name}); }; this.findParticipantById = function(participantId) { return _.findWhere(this.participants, {id: participantId}); }; this.removeParticipant = function(participantId) { this.participants = _.without(this.participants, _.findWhere(this.participants, {id: participantId})); }; }; Chat.prototype.newUser = function(data) { this.addParticipant(data); return { status: 'OK', participants: this.participants }; }; Chat.prototype.nameChange = function(data, participantId) { var participant = this.findParticipantById(participantId); if (!participant) { return { status: 'ERROR', situation: 'MISSING' }; } participant.name = data.name; return { status: 'OK' }; 42
  43. 43. }; Chat.prototype.disconnect = function(participantId) { this.removeParticipant(participantId); return { status: 'OK' }; }; module.exports = Chat; config.js var path = require('path'); var express = require('express'); // The configuration data. exports.data = { // Server configuration. server: { port: process.env.PORT || 5000 }, // Security configuration (not used for now). security: { cookieParserSecret: 'Secret' }, // Route configuration. route: { path: path.join(__dirname, 'routes') }, // Controller configuration. controller: { path: path.join(__dirname, 'controllers') }, // Model configuration. model: { path: path.join(__dirname, 'models') }, // View configuration. view: { path: path.join(__dirname, 'views'), engine: 'jade' }, // Style configuration. style: { path: path.join(__dirname, 'public'), engine: 'stylus' } } function configure(app) { // Common section. app.set('port', exports.data.server.port); 43
  44. 44. app.set('views', exports.data.view.path); app.set('view engine', exports.data.view.engine); app.use(express.logger('dev')); app.use(express.json()); app.use(express.urlencoded()); app.use(express.cookieParser(exports.data.security.cookieParserSecret)); app.use(express.session()); app.use(app.router); app.use(require(exports.data.style.engine).middleware(exports.data.style.path)); app.use(express.static(exports.data.style.path)); } // The configuration function for dev. exports.configDev = function(app) { configure(app); app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); }; // The configuration function for prod. exports.configProd = function(app) { configure(app); app.use(express.errorHandler()); }; http.js var http = require('http'); var app = require('./app'); var server = http.createServer(app); server.listen(app.get('port')); module.exports = server; io.js var _ = require('underscore'); var http = require('./http'); var io = require('socket.io').listen(http); var Repository = require('./repository'); var repository = new Repository(); var Chat = require('./chat'); var chat = new Chat(); // Socket events io.sockets.on('connection', function (socket) { // User related socket.on("newUser", function(data) { var result = chat.newUser(data); if (result.status == 'OK') { io.sockets.emit("newConnection", {participants: result.participants}); socket.emit("updateBranchCreated", repository.getActiveBranches()); } }); socket.on("nameChange", function(data) { var result = chat.nameChange(data, socket.id); 44
  45. 45. if (result.status == 'OK') { io.sockets.emit("nameChanged", {id: data.id, name: data.name}); return; } if (result.status == 'ERROR') { switch (result.situation) { case 'MISSING': socket.emit('error', {message: 'Could not change name for missing participant.'}); break; } } }); socket.on("disconnect", function() { var result = chat.disconnect(socket.id); if (result.status == 'OK') { io.sockets.emit("userDisconnected", {id: socket.id, sender: "system"}); } }); // Figure related socket.on("newFigure", function(data) { if (data.branch && repository.isBlocked(data.branch.name)) { io.sockets.emit("figureCreated", {id: socket.id, canvas: data.canvas, branch: data.branch}); } }); // Branch related socket.on("block", function(data) { var result = repository.block(data); if (result.status == 'OK') { socket.emit("blocked", { branch: { name: result.branch.name }, token: result.token }); return; } if (result.status == 'ERROR') { switch (result.situation) { case 'INVALID': socket.emit('error', {message: 'Invalid request'}); break; case 'MISSING': socket.emit('error', {message: 'Could not find branch ' + data.name}); break; case 'INUSE': socket.emit('error', {message: 'Could not block branch. There is another participant in control.'}); break; } 45
  46. 46. } }); socket.on("unblock", function(data) { var result = repository.unblock(data); if (result.status == 'OK') { socket.emit("unblocked", { branch: { name: result.branch.name } }); return; } if (result.status == 'ERROR') { switch (result.situation) { case 'INVALID': socket.emit('error', {message: 'Invalid request'}); break; case 'MISSING': socket.emit('error', {message: 'Could not find branch ' + data.name}); break; case 'FORBIDDEN': socket.emit('error', {message: 'Could not unblock branch. There is another participant in control.'}); break; } } }); socket.on("checkOutBranch", function(data) { var result = repository.checkOutBranch(data); if (result.status == 'OK') { socket.emit("branchCheckedOut", result.branch); return; } if (result.status == 'ERROR') { switch (result.situation) { case 'INVALID': socket.emit('error', {message: 'Invalid request'}); break; case 'MISSING': socket.emit('error', {message: 'Could not find branch ' + data.name}); break; } } }); socket.on("saveBranch", function(data) { var result = repository.saveBranch(data); if (result.status == 'OK') { switch (result.situation) { case 'CREATED': io.sockets.emit("updateBranchCreated", repository.getActiveBranches()); break; 46
  47. 47. case 'SAVED': socket.emit("branchSaved", {name: data.branch.name, automatic: data.automatic}); break; } return; } if (result.status == 'WARNING') { switch (result.situation) { case 'UPTODATE': socket.emit("warning", {message: "Did not save branch. Branch is already up to date."}); break; } return; } if (result.status == 'ERROR') { switch (result.situation) { case 'INVALID': socket.emit("error", {message: "Invalid request."}); break; case 'MISSING': socket.emit("error", {message: "Could not find branch " + data.branch.name + "."}); break; case 'CLIENTHEAD': socket.emit("error", {message: "Client error, branch head was not created."}); break; case 'SERVERHEAD': socket.emit("error", {message: "Server error, branch head was not created."}); break; case 'CONFLICT': socket.emit("error", {message: "Could not save branch due to another save. Checkout branch to get latest changes."}); break; case 'FORBIDDEN': socket.emit("error", {message: "Could not save branch. There is another participant in control. You may try to block the branch if it has been blocked for a long time."}); break; } } }); socket.on("deleteBranch", function(data) { var result = repository.deleteBranch(data); if (result.status == 'OK') { socket.emit("branchDeleted", data.name); // Remove the branch from the available branches in the client. io.sockets.emit("updateBranchDeleted", data.name); return; } if (result.status == 'ERROR') { 47
  48. 48. switch (result.situation) { case 'INVALID': socket.emit("error", {message: "Invalid request."}); break; case 'MISSING': socket.emit("error", {message: "Could not find branch " + data.name + "."}); break; case 'MASTER': socket.emit("error", {message: "Cannot delete branch master."}); break; } } }); }); module.exports = io; repository.js var _ = require('underscore'); var TIMEOUT_SECONDS = 300; function Repository() { this.branches = []; this.branches.push({ name: 'master', nodes: [{ id : { numeric: 0, author: {}, branch: 'master' }, object: {objects: [], background: ""}, next: {}, saved: true }], active: true }); this.blockades = []; this.findBranchByName = function(branchName) { return _.find(this.branches, function(branch) { return branch.name == branchName; }); }; this.removeBranch = function(branchName) { this.branches = _.filter(this.branches, function(branch) { return branch.name != branchName; }); }; this.addBranch = function(branch) { this.branches.push(branch); }; this.updateBranch = function(branch) { this.removeBranch(branch.name); this.addBranch(branch); }; 48
  49. 49. this.markSavedBranch = function(branch) { for (var index = branch.nodes.length - 1; index >= 0; --index) { if (branch.nodes[index].saved == false) { branch.nodes[index].saved = true; } else { return; } } }; this.removeBlockade = function(branchName) { this.blockades = _.filter(this.blockades, function(blockade) { return blockade.name != branchName; }); }; this.addBlockade = function(blockade) { this.blockades.push(blockade); }; this.findBlockade = function(branchName) { return _.find(this.blockades, function(blockade) { return blockade.name == branchName; }); }; }; Repository.prototype.isBlocked = function(branchName) { return this.findBlockade(branchName); }; Repository.prototype.block = function(data) { if (!data || !data.participant || !data.participant.id || !data.branch || !data.branch.name) { return { status: 'ERROR', situation: 'INVALID' }; } // See if the branch exists var branchName = data.branch.name; var foundBranch = this.findBranchByName(branchName); if (!foundBranch || foundBranch.active == false) { return { status: 'ERROR', situation: 'MISSING' }; } var now = new Date(); var participantId = data.participant.id; var blockade = this.findBlockade(branchName); if (blockade) { // Check that the blocking time has expired if (new Date(blockade.date.getTime() + TIMEOUT_SECONDS * 1000) < now) { // This user can take control, and is given a replace token blockade.participantId = participantId; var token = Math.random(); blockade.token = token; blockade.date = now; 49
  50. 50. return { status: 'OK', branch: { name: branchName }, token: token }; } var token = data.token; if (!token || blockade.token != token) { // This user has not authenticated return { status: 'ERROR', situation: 'INUSE' }; } // This user can prolong his blocking control, and is given a new token blockade.date = now; blockade.token = Math.random(); return { status: 'OK', branch: { name: branchName }, token: blockade.token }; } // This user can take control, and is given an initial token var token = Math.random(); this.addBlockade({ name: branchName, participantId: participantId, token: token, date: now }); return { status: 'OK', branch: { name: branchName }, token: token }; }; Repository.prototype.unblock = function(data) { if (!data || !data.participant || !data.participant.id || !data.branch || !data.branch.name) { return { status: 'ERROR', situation: 'INVALID' }; } if (!data.token) { return { status: 'ERROR', 50
  51. 51. situation: 'FORBIDDEN' }; } // See if the branch exists var branchName = data.branch.name; var foundBranch = this.findBranchByName(branchName); if (!foundBranch || foundBranch.active == false) { return { status: 'ERROR', situation: 'MISSING' }; } var participantId = data.participant.id; var token = data.token; var blockade = this.findBlockade(branchName); if (blockade && blockade.token != token) { return { status: 'ERROR', situation: 'FORBIDDEN' }; } this.removeBlockade(branchName); return { status: 'OK', branch: { name: branchName } }; }; Repository.prototype.getActiveBranches = function() { return _.where(this.branches, {active: true}); }; Repository.prototype.checkOutBranch = function(data) { if (!data || !data.name) { return { status: 'ERROR', situation: 'INVALID' }; } var branchName = data.name; var foundBranch = this.findBranchByName(branchName); if (!foundBranch || foundBranch.active == false) { return { status: 'ERROR', situation: 'MISSING' }; } return { status: 'OK', branch: foundBranch }; }; Repository.prototype.saveBranch = function(data) { if (!data || !data.branch || !data.branch.name) { 51
  52. 52. return { status: 'ERROR', situation: 'INVALID' }; } var branchName = data.branch.name; var foundBranch = this.findBranchByName(branchName); if (!foundBranch) { // I. Branch was not saved before => just save it now. this.markSavedBranch(data.branch); this.updateBranch(data.branch); return { status: 'OK', situation: 'CREATED' }; } if (foundBranch.active == false) { return { status: 'ERROR', situation: 'MISSING' }; } var blockade = this.findBlockade(branchName); if (blockade) { var token = data.token; if (!token || blockade.token != token) { return { status: 'ERROR', situation: 'FORBIDDEN' }; } } var serverBranch = foundBranch; var clientBranch = data.branch; if (clientBranch.nodes.length <= 0) { return { status: 'ERROR', situation: 'CLIENTHEAD' }; } if (serverBranch.nodes.length <= 0) { return { status: 'ERROR', situation: 'SERVERHEAD' }; } var min = Math.min(clientBranch.nodes.length, serverBranch.nodes.length); if (clientBranch.nodes.length > min) { if (JSON.stringify(clientBranch.nodes[min - 1].id) == JSON.stringify(serverBranch.nodes[min - 1].id)) { // II.2. Client made changes, server did not => add change to branch. this.markSavedBranch(data.branch); 52
  53. 53. this.updateBranch(data.branch); return { status: 'OK', situation: 'SAVED' }; } // II.4. Both made changes, reject change. return { status: 'ERROR', situation: 'CONFLICT' }; } if (serverBranch.nodes.length > min) { if (JSON.stringify(clientBranch.nodes[min - 1].id) == JSON.stringify(serverBranch.nodes[min - 1].id)) { // II.3. Server made changes, client did not => do nothing. return { status: 'WARNING', situation: 'UPTODATE' }; } // II.4. Both made changes, reject change. return { status: 'ERROR', situation: 'CONFLICT' }; } if (JSON.stringify(clientBranch.nodes[min - 1].id) == JSON.stringify(serverBranch.nodes[min - 1].id)) { // II.1. Nobody made changes => do nothing. return { status: 'WARNING', situation: 'UPTODATE' }; } return { status: 'ERROR', situation: 'CONFLICT' }; }; Repository.prototype.deleteBranch = function(data) { if (!data || !data.name) { return { status: 'ERROR', situation: 'INVALID' }; } var branchName = data.name; if (branchName == 'master') { return { status: 'ERROR', situation: 'MASTER' }; 53
  54. 54. } var foundBranch = this.findBranchByName(branchName); if (!foundBranch || foundBranch.active == false) { return { status: 'ERROR', situation: 'MISSING' }; } // Deactivate the branch. foundBranch.active = false; return { status: 'OK' }; }; module.exports = Repository; routes.js var _ = require('underscore'); module.exports.configure = function(app, io) { // GET method to obtain the index view app.get("/", function(request, response) { response.render("index"); }); // POST method to create a chat message app.post("/message", function(request, response) { var message = request.body.message; if(_.isUndefined(message) || _.isEmpty(message.trim())) { return response.json(400, {error: "Message is invalid"}); } var name = request.body.name; io.sockets.emit("incomingMessage", {message: message, name: name}); response.json(200, {message: "Message received"}); }); } server.js var app = require('./app'); var io = require('./io'); var routes = require('./routes'); routes.configure(app, io); B.2. Client side code circle.js function CircleDrawer(canvas, modifier, socket) { var iniPos; 54
  55. 55. var previewCircle; var pressed = false; this.mouseDown = function(options) { iniPos = { x: options.e.clientX, y: options.e.clientY }; pressed = true; }; this.mouseMove = function(options) { if (!pressed) { return; } var radius = Math.sqrt(Math.pow(options.e.clientX - iniPos.x, 2) + Math.pow(options.e.clientY - iniPos.y, 2)); canvas.remove(previewCircle); previewCircle = new fabric.Circle({ strokeWidth: modifier.get('preview-stroke-width'), stroke: modifier.get('preview-stroke-color'), fill: modifier.get('preview-fill-color'), left: iniPos.x - radius, top: iniPos.y - radius, radius: radius }); canvas.add(previewCircle); }; this.mouseUp = function(options) { pressed = false; var radius = Math.sqrt(Math.pow(options.e.clientX - iniPos.x, 2) + Math.pow(options.e.clientY - iniPos.y, 2)); canvas.remove(previewCircle); if (iniPos.x == options.e.clientX && iniPos.x == options.e.clientX) { return; } var circle = new fabric.Circle({ strokeWidth: modifier.get('actual-stroke-width'), stroke: modifier.get('actual-stroke-color'), fill: modifier.get('actual-fill-color'), left: iniPos.x - radius, top: iniPos.y - radius, radius: radius }); canvas.add(circle); socket.figureCreated(canvas); }; }; ellipse.js function EllipseDrawer(canvas, modifier, socket) { var pressed = false; var previewEllipse; var iniPos; this.mouseDown = function(options) { 55
  56. 56. iniPos = { x: options.e.clientX, y: options.e.clientY }; pressed = true; }; this.mouseMove = function(options) { if (!pressed) { return; } canvas.remove(previewEllipse); var radiusx = Math.abs(options.e.clientX - iniPos.x); var radiusy = Math.abs(options.e.clientY - iniPos.y); previewEllipse = new fabric.Ellipse({ left:iniPos.x - radiusx, top: iniPos.y - radiusy, strokeWidth: modifier.get('preview-stroke-width'), stroke: modifier.get('preview-stroke-color'), fill: modifier.get('preview-fill-color'), rx: radiusx, ry: radiusy }); canvas.add(previewEllipse); }; this.mouseUp = function(options) { pressed = false; canvas.remove(previewEllipse); var radiusx = Math.abs(options.e.clientX - iniPos.x); var radiusy = Math.abs(options.e.clientY - iniPos.y); var ellipse = new fabric.Ellipse({ left: iniPos.x - radiusx, top: iniPos.y - radiusy, strokeWidth: modifier.get('actual-stroke-width'), stroke: modifier.get('actual-stroke-color'), fill: modifier.get('actual-fill-color'), rx: radiusx, ry: radiusy }); canvas.add(ellipse); socket.figureCreated(canvas); }; }; line.js function LineDrawer(canvas, modifier, socket) { var pressed = false; var previewLine; var iniPos; this.mouseDown = function(options) { iniPos = { x1: options.e.clientX, y1: options.e.clientY }; pressed = true; }; 56
  57. 57. this.mouseMove = function(options) { if (!pressed) { return; } canvas.remove(previewLine); previewLine = new fabric.Line([iniPos.x1, iniPos.y1, options.e.clientX, options.e.clientY], { strokeWidth: modifier.get('preview-stroke-width'), stroke: modifier.get('preview-stroke-color'), }); canvas.add(previewLine); }; this.mouseUp = function(options) { pressed = false; canvas.remove(previewLine); var line = new fabric.Line([iniPos.x1, iniPos.y1, options.e.clientX, options.e.clientY], { strokeWidth: modifier.get('actual-stroke-width'), stroke: modifier.get('actual-stroke-color'), }); canvas.add(line); socket.figureCreated(canvas); }; }; polyline.js function PolylineDrawer(canvas, modifier, socket) { var points = []; var previewLines = []; this.mouseUp = function(options) { points.push({x: options.e.clientX, y: options.e.clientY}); if (points.length <= 1) { return; } var point1 = points[points.length - 2]; var point2 = points[points.length - 1]; var previewLine = new fabric.Line([point1.x, point1.y, point2.x, point2.y],{ strokeWidth: modifier.get('preview-stroke-width'), stroke: modifier.get('preview-stroke-color') }); previewLines.push(previewLine); canvas.add(previewLine); }; this.stateChanged = function() { for (var index = 0; index < previewLines.length; ++index) { var previewLine = previewLines[index]; 57
  58. 58. canvas.remove(previewLine); canvas.add(new fabric.Line([previewLine.x1, previewLine.y1, previewLine.x2, previewLine.y2], { strokeWidth: modifier.get('actual-stroke-width'), stroke: modifier.get('actual-stroke-color'), })); } socket.figureCreated(canvas); while (previewLines.length > 0) { previewLines.pop(); } } }; rectangle.js function RectangleDrawer(canvas, modifier, socket) { var pressed = false; var previewRect; var iniPos; this.mouseDown = function(options) { iniPos = { x: options.e.clientX, y: options.e.clientY }; pressed = true; }; this.mouseMove = function(options) { if (!pressed) { return; } canvas.remove(previewRect); previewRect = new fabric.Rect({ left: iniPos.x, top: iniPos.y, strokeWidth: modifier.get('preview-stroke-width'), stroke: modifier.get('preview-stroke-color'), fill: modifier.get('preview-fill-color'), width: options.e.clientX - iniPos.x, height: options.e.clientY - iniPos.y }); canvas.add(previewRect); }; this.mouseUp = function(options) { pressed = false; canvas.remove(previewRect); var rect = new fabric.Rect({ left: iniPos.x, top: iniPos.y, strokeWidth: modifier.get('actual-stroke-width'), stroke: modifier.get('actual-stroke-color'), fill: modifier.get('actual-fill-color'), width: options.e.clientX - iniPos.x, height: options.e.clientY - iniPos.y }); 58
  59. 59. canvas.add(rect); socket.figureCreated(canvas); }; }; text.js function TextDrawer(canvas, modifier, socket) { this.mouseUp = function(options) { var text = prompt("Enter text"," "); if (text != null) { var textbox = new fabric.Text(text, { left: options.e.clientX, top: options.e.clientY }); canvas.add(textbox); socket.figureCreated(canvas); } }; }; triangle.js function TriangleDrawer(canvas, modifier, socket) { var pressed = false; var previewTriangle; var iniPos; this.mouseDown = function(options) { iniPos = { x: options.e.clientX, y: options.e.clientY }; pressed = true; }; this.mouseMove = function(options) { if (!pressed) { return; } canvas.remove(previewTriangle); previewTriangle = new fabric.Triangle({ left: iniPos.x, top: iniPos.y, strokeWidth: modifier.get('preview-stroke-width'), stroke: modifier.get('preview-stroke-color'), fill: modifier.get('preview-fill-color'), width: options.e.clientX - iniPos.x, height: options.e.clientY - iniPos.y }); canvas.add(previewTriangle); }; this.mouseUp = function(options) { pressed = false; canvas.remove(previewTriangle); if (iniPos.x == options.e.clientX && iniPos.x == options.e.clientX) { 59
  60. 60. return; } var triangle = new fabric.Triangle({ left: iniPos.x, top: iniPos.y, strokeWidth: modifier.get('actual-stroke-width'), stroke: modifier.get('actual-stroke-color'), fill: modifier.get('actual-fill-color'), width: options.e.clientX - iniPos.x, height: options.e.clientY - iniPos.y }); canvas.add(triangle); socket.figureCreated(canvas); }; }; chat.js function Chat() { this.sessionId = ''; }; Chat.prototype.getSessionId = function() { return this.sessionId; } Chat.prototype.setSessionId = function(newSessionId) { this.sessionId = newSessionId; } index.js $(document).on('ready', function() { $('#merge').hide(); $('#workspace').hide(); var canvas = new fabric.Canvas('demo'); var modifier = new Modifier(); setupModifier(modifier); setupModifierButtonEvents(modifier); var painter = new Painter(canvas); var chat = new Chat(); var ui = new UI(); var repository = new Repository(ui); var socket = new Socket(document.domain, canvas, chat, repository, ui); registerFigures(canvas, painter, modifier, socket); registerFigureButtonEvents(painter); registerCanvasEvents(canvas, painter); setupChatControls(socket, ui); setupBranchButtons(socket, canvas); }); function setupModifier(modifier) { 60
  61. 61. // Specify preview shape attributes modifier.set('preview-stroke-width', 10); modifier.set('preview-stroke-color', 'gray'); modifier.set('preview-fill-color', 'lightgray'); // Specify actual attributes modifier.set('actual-stroke-width', 10); modifier.set('actual-stroke-color', 'black'); modifier.set('actual-fill-color', 'transparent'); }; function registerFigures(canvas, painter, modifier, socket) { painter.registerDrawer('circle', new CircleDrawer(canvas, modifier, socket)); painter.registerDrawer('ellipse', new EllipseDrawer(canvas, modifier, socket)); painter.registerDrawer('rectangle', new RectangleDrawer(canvas, modifier, socket)); painter.registerDrawer('triangle', new TriangleDrawer(canvas, modifier, socket)); painter.registerDrawer('line', new LineDrawer(canvas, modifier, socket)); painter.registerDrawer('polyline', new PolylineDrawer(canvas, modifier, socket)); painter.registerDrawer('text', new TextDrawer(canvas, modifier, socket)); }; function registerFigureButtonEvents(painter) { $('#circle').on('click', function() { painter.setDrawer('circle') }); $('#ellipse').on('click', function() { painter.setDrawer('ellipse') }); $('#rectangle').on('click', function() { painter.setDrawer('rectangle') }); $('#triangle').on('click', function() { painter.setDrawer('triangle') }); $('#line').on('click', function() { painter.setDrawer('line') }); $('#polyline').on('click', function() { painter.setDrawer('polyline') }); $('#text').on('click', function() { painter.setDrawer('text') }); }; function registerCanvasEvents(canvas, painter) { canvas.on('mouse:down', function(options) { painter.mouseDown(options); }); canvas.on('mouse:move', function(options) { painter.mouseMove(options); }); canvas.on('mouse:up', function(options) { painter.mouseUp(options); }); canvas.on('object:moving', function(options) { painter.clearDrawer(); }); canvas.on('object:scaling', function(options) { painter.clearDrawer(); }); canvas.on('object:rotating', function(options) { painter.clearDrawer(); }); canvas.on('object:modified', function(options) { painter.clearDrawer(); }); }; function setupModifierButtonEvents(modifier) { // Setup the stroke color picker $('#strokeColor').colorpicker(); $('#strokeColor').colorpicker().on('changeColor', function(ev) { var color = ev.color.toRGB(); modifier.set('actual-stroke-color', 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + color.a + ')'); }); // Setup the fill color picker $('#fillColor').colorpicker(); $('#fillColor').colorpicker().on('changeColor', function(ev) { var color = ev.color.toRGB(); modifier.set('actual-fill-color', 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + color.a + ')'); }); // Setup the stroke size slider 61
  62. 62. $("#strokeWidth").change(function() { modifier.set('preview-stroke-width', $(this).val()); modifier.set('actual-stroke-width', $(this).val()); }); }; function setupChatControls(socket, ui) { $('#outgoingMessage').on('keydown', ui.outgoingMessageKeyDown); $('#outgoingMessage').on('keyup', ui.outgoingMessageKeyUp); $('#name').on('focusout', socket.nameFocusOut); $('#send').on('click', ui.sendMessage); }; function setupBranchButtons(socket, canvas) { $('#checkOutBranch').on('click', function() { var name = $('#branches button.selected').html(); if (name) { socket.checkOutBranch(name); } else { alert("You did not select a branch from the list."); } }); $('#createBranch').on('click', function() { var name = prompt("Please enter a branch name", ""); if (name) { socket.createBranch(name); } else { alert("You did not specify a name."); } }); $('#deleteBranch').on('click', function() { var name = $('#branches button.selected').html(); if (name) { socket.deleteBranch(name); } else { alert("You did not select a branch from the list."); } }); $('#commitBranch').on('click', function() { var obj = canvas.toDatalessJSON(); socket.commitBranch(obj, false); }); $('#mergeBranch').on('click', function() { var name = $('#branches button.selected').html(); if (name) { socket.mergeBranch(name); } else { alert("You did not select a branch from the list."); } }); $('#saveBranch').on('click', function() { socket.saveBranch(); }); $('#block').on('click', function() { socket.block(); }); 62
  63. 63. $('#unblock').on('click', function() { socket.unblock(); }); } modifier.js function Modifier(canvas) { var properties = []; this.get = function(propertyName) { return properties[propertyName]; } this.set = function(propertyName, propertyValue) { properties[propertyName] = propertyValue; } }; painter.js function Painter(canvas) { // Register drawer types var currentDrawerName; var drawers = []; this.registerDrawer = function(drawerName, drawer) { drawers[drawerName] = drawer; } this.setDrawer = function(drawerName) { if (!drawers[drawerName]) { throw "Illegal argument exception: " + drawerName + " is not registered!"; } for (var drawer in drawers) { if (!drawers[drawer].stateChanged) { continue; } drawers[drawer].stateChanged(); } currentDrawerName = drawerName; } this.clearDrawer = function() { currentDrawerName = ""; } this.mouseDown = function(options) { if (!drawers[currentDrawerName]) { return; } if (!drawers[currentDrawerName].mouseDown) { return; } 63
  64. 64. drawers[currentDrawerName].mouseDown(options); } this.mouseMove = function(options) { if (!drawers[currentDrawerName]) { return; } if (!drawers[currentDrawerName].mouseMove) { return; } drawers[currentDrawerName].mouseMove(options); } this.mouseUp = function(options) { if (!drawers[currentDrawerName]) { return; } if (!drawers[currentDrawerName].mouseUp) { return; } drawers[currentDrawerName].mouseUp(options); } }; repository.js function Repository(ui) { this.ui = ui; this.branches = []; this.blockades = []; this.clientBranch; this.removeBlockade = function(branchName) { this.blockades = _.filter(this.blockades, function(blockade) { return blockade.name != branchName; }); }; this.addBlockade = function(blockade) { this.blockades.push(blockade); }; this.findBlockade = function(branchName) { return _.find(this.blockades, function(blockade) { return blockade.name == branchName; }); }; this.removeBranch = function(branchName) { this.branches = _.filter(this.branches, function(branch) { return branch.name != branchName; }); }; this.addBranch = function(branch) { this.branches.push(branch); } }; Repository.prototype.getBranches = function() { 64

×