Room Arranger, a sample web application

The Room Arranger web application should allow:

Go to the application.

Download complete example

Client-side

The web applications consists of:

Guess the size of the client side application? Guess how long it took to develop?

room.html

room.html
<html>
<head>
<title>Room Arranger</title>
<link rel="stylesheet" href="room.css" type="text/css"/>
<script type="text/javascript" src="room.js">
</script>
</head>
<body>

<h1> Room Arranger </h1>

<button id='save'> Save room </button> 
<button id='load'> Load room </button> <br>

<div class="container">
    <div class="room-plan" id="room">
    </div>

    <div class="tab-panel">
	<div class="tabs">
	    <span class="selected">room</span>
	    <span>bureau</span>
	    <span>bed</span>
	</div>
	<div class="panel-frame">
	    <div class="panel" style="display : block;">
		<form class="desc" onsubmit="return noaction()">
		    <table>
		    <tr>
		       <td class="label"><label for="r_name">Name</label></td>
		       <td class="input"><input name="r_name" id="r_name"></td>
		    </tr>
		    <tr>
			<td class="label"><label for="r_width">Width</label>
			<td class="input"><input name="r_width" id="r_width">
		    </tr>
		    <tr>
			<td class="label"><label for="r_length">Length</label></td>
			<td class="input"><input name="r_length" id="r_length"></td>
		    </tr>
		    </table>
		    <button id="update-room">Update Room</button>
		</form>
	    </div>
	    <div class="panel">
		<form class="desc" onsubmit="return noaction()">
		    <table>
		    <tr>
		       <td class="label"><label for="u_name">Name</label></td>
		       <td class="input"><input name="u_name" id="u_name"></td>
		    </tr>
		    <tr>
			<td class="label"><label for="u_width">Width</label>
			<td class="input"><input name="u_width" id="u_width">
		    </tr>
		    <tr>
			<td class="label"><label for="u_length">Length</label></td>
			<td class="input"><input name="u_length" id="u_length"></td>
		    </tr>
		    </table>
		    <button id="add-bureau"> Add Bureau</button>
		</form>
	    </div>
	    <div class="panel">
		<form class="desc" onsubmit="return noaction()">
		    <table>
		    <tr>
		       <td class="label"><label for="b_name">Name</label></td>
		       <td class="input"><input name="b_name" id="b_name"></td>
		    </tr>
		    <tr>
			<td class="label"><label for="b_type">Type</label></td>
			<td class="input">
			    <select name="b_type" id="b_type">
				<option value="twin">twin</option>
				<option value="full">full</option>
				<option value="queen">queen</option>
				<option value="king">king</option>
			    </select>
			</td>
		    </tr>
		    </table>
		    <button id="add-bed">Add Bed</button>
		</form>
	    </div>
	</div>
    </div>

</div> <!-- container -->

<div id="result"> </div>
</body>
</html>

93 lines.

room.css

room.css
/* form style */
form.desc td.label { 
    text-align: right;
}

/* tab-panel widget */
div.tabs {
    margin-bottom: 0;
}

div.tabs span {
	padding-left: 0.4em;
	padding-right: 0.4em;
	border-top: 3px solid black;
	border-left: 3px solid black;
	border-right: 3px solid black;
	-moz-border-radius-topleft: 1em;
	-moz-border-radius-topright: 1em;
	background-color: #FFFFFF;
	cursor: default;
}

div.tabs span:hover {
	background-color: #00FFFF;
}

div.tabs span.selected {
	padding-left: 0.4em;
	padding-right: 0.4em;
	border-top: 3px solid black;
	border-left: 3px solid black;
	border-right: 3px solid black;
	-moz-border-radius-topleft: 1em;
	-moz-border-radius-topright: 1em;
	background-color: #FF33FF;
}

div.tabs span.selected:hover {
	background-color: #FFAAFF;
}

div.panel-frame {
    border: 3px solid black;
    height : 300px;
    padding-top : 5px;
    padding-left : 5px;
}

div.tab-panel {
    width: 40%;
    float: right;
    margin : 10px;
}

/* by default, hide any nonselected form */
div.panel {
    display : none;
}

/*  border around tabs and plan for decoration */
div.container {
    border: 1px solid blue;
    overflow: auto;
    width: 100%
}


/* room and  holdings */

div.room-plan {
    width : 56%;
    height : 500px;
    float: left;
    overflow : hidden;
    position : relative;
    border : red solid 2px;
    margin : 5px;
}

div.room {
    border : solid black 4px;
    position : absolute;
}

div.bureau {
    border : solid red 4px;
    /* background-color : rgb(255,22,22); */
    position : absolute;
}

div.bed {
    border : solid yellow 4px;
    /* background-color : rgb(255,22,22); */
    position : absolute;
}

95 lines.

room.js

room.js
/* global application object */
// classes for the application

function Application() {
    this.element = null; // element being dragged
    this.startX = 0;
    this.startY = 0;
    this.posX = 0;
    this.posY = 0;
    this.maxWidth = 0;
    this.maxHeight = 0;

    this.scale = 500.0 / 20.0; // 500 pixels divided by 20 feet
}

var application = new Application()

Application.prototype.toPixel = function( measure )  {
    return measure*this.scale;
};

Application.prototype.toFeet = function( measure )  {
    return measure/this.scale;
};

Application.prototype.getRoomTop = function( room )  {
    var p = Application.getRoomContainer();
    var t = (p.offsetHeight - this.toPixel(room.length)) / 2.0;
    return t;
};

Application.prototype.getRoomLeft = function( room )  {
    var p = Application.getRoomContainer();
    var l = (p.offsetWidth - this.toPixel(room.width)) / 2.0;
    return l;
};

Application.getRoomContainer = function() {
    return document.getElementById('room' );
};

Application.getRoomNode = function() {
    var c = Application.getRoomContainer();
    var divs = c.getElementsByTagName('div');
    if ( divs.length == 0 ) return null;
    return divs[0];
};

Application.dragAction = function( evt ) {
    if ( application.element == null ) {
	Application.dragStop( evt )
        return 
    }
    var o = application.element
    var dx = (evt.clientX-application.startX)
    var dy = (evt.clientY-application.startY)
    var x = (application.posX + dx) 
    // need to handle padding
    if ( x > application.maxWidth ) x = application.maxWidth;
    else if ( x < 0 ) x = 0;
    var y =  (application.posY + dy) 
    if ( y > application.maxHeight ) y = application.maxHeight;
    else if ( y < 0 ) y = 0;
    o.style.left = x + "px";
    o.style.top = y + "px";

    // update the specific movable piece
    var movable = application.element.movable;
    movable.top = application.toFeet( application.element.offsetTop );
    movable.left = application.toFeet( application.element.offsetLeft );
    //dump( movable + '\n' )
    //dump( movable.left + " " + movable.top + '\n' )
};

Application.dragStop = function( evt ) {
    application.element = null;
    var r = application.parent;
    r.removeEventListener('mousemove', Application.dragAction, true );
    r.removeEventListener('mouseup', Application.dragStop, true );
    evt.preventDefault();
};

Application.dragStart = function( evt ) {
    application.element = evt.target;
    application.startX = evt.clientX;
    application.startY = evt.clientY;
    application.posX = evt.target.offsetLeft;
    application.posY = evt.target.offsetTop;
    application.parent = evt.target.parentNode;
    application.parent.addEventListener('mousemove', Application.dragAction, true );
    application.parent.addEventListener('mouseup', Application.dragStop, true );
    application.maxWidth = application.parent.offsetWidth - application.element.offsetWidth;
    application.maxHeight = application.parent.offsetHeight - application.element.offsetHeight;
    evt.preventDefault();
};

Application.updateRoomHandler = function( evt ) {
    var form = evt.target.form;
    var n = form.r_name.value;
    var w = parseFloat( form.r_width.value);
    var l = parseFloat( form.r_length.value );
    var roomNode = Application.getRoomNode();
    if ( !roomNode ) {
	var container = Application.getRoomContainer();
	var r = new Room( n, w, l );
	roomNode = r.createHTML();
	container.appendChild( roomNode );
	r.updatePosition( roomNode );
    }
    else {
        roomNode.room.name = n;
        roomNode.room.width = w;
	roomNode.room.length = l;
	roomNode.room.updatePosition( roomNode );
	roomNode.room.setName( roomNode );
    }
}

Application.addBureauHandler = function( evt ) {
    var form = evt.target.form;
    var n = form.u_name.value;
    var w = parseFloat( form.u_width.value);
    var l = parseFloat( form.u_length.value );
    var roomNode = Application.getRoomNode();
    if ( roomNode ) {
	var b = new Bureau( n, w, l );
	var bNode = b.createHTML();
	roomNode.appendChild( bNode );
    }
}

Application.addBedHandler = function( evt ) {
    var form = evt.target.form;
    var n = form.b_name.value;
    var t = form.b_type.value;
    var roomNode = Application.getRoomNode();
    if ( roomNode ) {
	var b = new Bed( n, t );
	var bNode = b.createHTML();
	roomNode.appendChild( bNode );
    }
}

Application.loadRoom = function() {
    var p = Application.getRoomContainer();
    // get rid of old room
    if ( p ) {
        var c = p.firstChild;
	while ( c ) {
	    var t = c;
	    c = c.nextSibling;
	    p.removeChild( t );
	}
    }
    // retrieve saved room from server
    var req = new XMLHttpRequest();
    req.open("GET","cgi-bin/loadroom.sh", true );
    req.onreadystatechange = function() {
        if ( req.readyState == 4) {
	    var root = req.responseXML.documentElement;
	    var room = Room.createFromXMLNode( root );
	    // build HTML representation of layout
	    var roomNode = room.createHTML();
	    p.appendChild( roomNode );
	    room.updatePosition( roomNode );
	    // add all the movable elements to the room
	    var e = root.firstChild;
	    while ( e ) {
		if ( e.nodeName == 'bureau' ) {
		    var bureau = Bureau.createFromXMLNode( e );
		    roomNode.appendChild( bureau.createHTML() );
		}
		else if ( e.nodeName == 'bed' ) {
		    var bed = Bed.createFromXMLNode( e );
		    roomNode.appendChild( bed.createHTML() );
		}
	        e = e.nextSibling;
	    }
	}
    }
    req.send( null );
}

Application.saveRoom = function() {
    var xmlDoc = document.implementation.createDocument("", "", null);
    var roomNode = Application.getRoomNode();
    if ( !roomNode ) return;
    // create the XML description of the room layout
    var xml = roomNode.room.toXMLNode( xmlDoc );
    var divs = roomNode.getElementsByTagName( 'div' );
    for( var i = 0 ; i < divs.length; i++ ) {
        var m = divs[i].movable;
	if ( m ) {
	    xml.appendChild( m.toXMLNode( xmlDoc ) );
	}
    }
    xmlDoc.appendChild( xml );

    // transmit the document
    var req = new XMLHttpRequest();
    req.open("POST","cgi-bin/saveroom.sh", true );
    req.onreadystatechange = function() {
        if ( req.readyState == 4) {
	    displayResult( req )
	}
    }
    req.send( xmlDoc );
}

// extract and display the results
function displayResult( req ) {
    var d = req.responseXML.documentElement
    // since there is only one text value, the answer
    // can be extract with textContent
    var ans = d.textContent
    document.getElementById('result').innerHTML = ans
}


/* ---------------------- form widget ------------------------- */
function mkShowPanelHandler( tab, panel, tabs, panels ) {
    var handler = function( evt ) {
	for( var i = 0, len=panels.snapshotLength; i < len; i++ ) {
	    var p = panels.snapshotItem( i );
	    var t = tabs.snapshotItem( i );
	    if ( p != panel ) {
		p.style.display = 'none';
	    }
	    if ( t != tab ) {
	        t.className = ''
	    }
	}
	panel.style.display = 'block';
	tab.className = 'selected'
    }
    tab.addEventListener("click", handler, false);
}

function initializeTabPanel( tab_panel ) {
    var tabs = document.evaluate(
                  "div[@class='tabs']/span",
		  tab_panel, null,
		  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null );
    var panels = document.evaluate(
                  "div[@class='panel-frame']/div[@class='panel']",
		  tab_panel, null,
		  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null );
    // tabs.snapshotLength must equal panels.snapshotLength
    for( var i = 0, len=tabs.snapshotLength; i < len; i++ ) {
        var tab = tabs.snapshotItem( i );
        var panel = panels.snapshotItem( i );
	mkShowPanelHandler( tab, panel, tabs, panels )
    }
}
/* ---------------------- end form widget ------------------------- */
/* models the moveable 'things' in the room */

// room  object
function Room( name, width, length ){
    this.name = name;
    this.width = width;
    this.length = length;
}

Room.prototype.toXMLNode = function( doc ) {
    var res = doc.createElement('room');
    res.setAttribute('name', this.name );
    res.setAttribute('width', this.width.toString() );
    res.setAttribute('length', this.length.toString() );
    return res;
};

Room.createFromXMLNode = function( element ) {
    var n = element.getAttribute( 'name' );
    var w = parseFloat( element.getAttribute( 'width' ) );
    var l = parseFloat( element.getAttribute( 'length' ) );
    return new Room(n, w, l );
};

Room.prototype.createHTML = function() {
    var e = document.createElement( 'div' );
    var title = document.createElement('span');
    title.innerHTML = this.name;
    e.appendChild( title );
    e.className = 'room';
    e.room = this; // attach room to node
    return e;
};

Room.prototype.setName = function( node ) {
    var spans = node.getElementsByTagName('span' );
    if ( spans.length != 0 ) {
        spans[0].innerHTML = this.name;
    }
};

Room.prototype.updatePosition = function( node ) {
    node.style.width = application.toPixel( this.width ) + 'px';
    node.style.height = application.toPixel( this.length ) + 'px';
    node.style.top = application.getRoomTop( this ) + 'px';
    node.style.left = application.getRoomLeft( this ) + 'px';
};

// bureau object

function Bureau( name, width, length ){
    this.name = name;
    this.width = width;
    this.length = length;
    this.top = 0;
    this.left = 0;
}

Bureau.prototype.toXMLNode = function( doc ) {
    var res = doc.createElement('bureau');
    res.setAttribute('name', this.name );
    res.setAttribute('width', this.width.toString() );
    res.setAttribute('length', this.length.toString() );
    res.setAttribute('top', this.top.toString() );
    res.setAttribute('left', this.left.toString() );
    return res;
};

Bureau.createFromXMLNode = function( element ) {
    var n =  element.getAttribute( 'name' );
    var w = parseFloat( element.getAttribute( 'width' ) );
    var l = parseFloat( element.getAttribute( 'length' ) );
    var top = parseFloat( element.getAttribute( 'top' ) );
    var left = parseFloat( element.getAttribute( 'left' ) );
    var b = new Bureau(n, w, l );
    b.top = top;
    b.left = left;
    return b;
};

Bureau.prototype.createHTML = function() {
    var e = document.createElement( 'div' );
    e.className = 'bureau';
    e.style.width = application.toPixel( this.width ) + 'px';
    e.style.height = application.toPixel( this.length ) + 'px';
    this.updatePosition( e );
    e.addEventListener("mousedown", Application.dragStart, false );
    e.movable = this; 
    return e;
};

Bureau.prototype.updatePosition = function( node ) {
    node.style.top = application.toPixel( this.top ) + 'px';
    node.style.left = application.toPixel( this.left ) + 'px';
};

// bureau object

function Bed( name, size ){
    this.name = name;
    this.setSize(size) ;
    this.top = 0;
    this.left = 0;
}

Bed.prototype.setSize = function( size ) {
    this.size = size;
    if ( size == 'twin' ) {
	this.width = 38.0 / 12.0;
	this.length = 75.0 / 12.0;
    }
    else if ( size == 'full' || size == 'double') {
	this.width = 53.0 / 12.0;
	this.length = 75.0 / 12.0;
    }
    else if ( size == 'queen' ) {
	this.width = 60.0 / 12.0;
	this.length = 80.0 / 12.0;
    }
    else if ( size == 'king' ) {
	this.width = 76.0 / 12.0;
	this.length = 80.0 / 12.0;
    }
}

Bed.prototype.toXMLNode = function( doc ) {
    var res = doc.createElement('bed');
    res.setAttribute('name', this.name );
    res.setAttribute('size', this.size );
    res.setAttribute('top', this.top.toString() );
    res.setAttribute('left', this.left.toString() );
    return res;
};

Bed.createFromXMLNode = function( element ) {
    var n =  element.getAttribute( 'name' );
    var s =  element.getAttribute( 'size' );
    var top = parseFloat( element.getAttribute( 'top' ) );
    var left = parseFloat( element.getAttribute( 'left' ) );
    var b = new Bed( n, s );
    b.top = top;
    b.left = left;
    return b;
};

Bed.prototype.createHTML = function() {
    var e = document.createElement( 'div' );
    e.className = 'bed';
    e.style.width = application.toPixel( this.width ) + 'px';
    e.style.height = application.toPixel( this.length ) + 'px';
    e.addEventListener("mousedown", Application.dragStart, false );
    this.updatePosition( e );
    e.movable = this; 
    return e;
};

Bed.prototype.updatePosition = function( node ) {
    node.style.top = application.toPixel( this.top ) + 'px';
    node.style.left = application.toPixel( this.left ) + 'px';
};

function noaction(e) { return false; }

// set up the event handling

function initializeUserInterface() {
    var tab_panels = document.evaluate(
		"//div[@class='tab-panel']",
		document, null,
		XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); 
    for ( var i = 0, len=tab_panels.snapshotLength; i < len; i++ )  {
        initializeTabPanel( tab_panels.snapshotItem( i ) );
    }

    var u = document.getElementById( 'update-room' );
    u.addEventListener('click', Application.updateRoomHandler, false );
    var b = document.getElementById( 'add-bureau' );
    b.addEventListener('click', Application.addBureauHandler, false )
    var bed = document.getElementById( 'add-bed' );
    bed.addEventListener('click', Application.addBedHandler, false )

    var save = document.getElementById( 'save' );
    save.addEventListener('click', Application.saveRoom, false )
    var load = document.getElementById( 'load' );
    load.addEventListener('click', Application.loadRoom, false )
}

// use bubble for load events, do not use capture
window.addEventListener("load", initializeUserInterface, false);

444 lines.

XML for the room's arrangement

room-desc.xml
<?xml version="1.0"?>
<room name="room1" width="18" length="10">
  <bureau name="old" width="1.5" length="2" top="0.64" left="15.92"/>
  <bed name="child1" size="twin" top="3.24" left="0"/>
  <bed name="child2" size="full" top="3.32" left="13.24"/>
  <bureau name="new" width="2" length="3" top="6.32" left="4.44"/>
</room>

Are top and bottom good names?

The server side

Python provides a simple web server that can be used for testing. The code for the server is:

mini-webserver.py
#!/usr/bin/python
import sys
import CGIHTTPServer
import BaseHTTPServer

# Use supplied port
if sys.argv[1:] :
    port = int(sys.argv[1])
else :
    port = 8000

#
# set up and run the server
#
addr = ('',port)
handler = CGIHTTPServer.CGIHTTPRequestHandler
httpd = BaseHTTPServer.HTTPServer(addr, handler)
sa = httpd.socket.getsockname()
print "Serving HTTP on", sa[0], "port", sa[1], "..."
httpd.serve_forever()

The server is run with:

python mini-webserver.py
The web server is run in a directory containing room.html, room.css, room.js, index.html, and cgi-bin.

Saving the arrangement

A simple CGI program that saves the arrangement is:

SaveRoom.java
import java.io.FileOutputStream;
import java.io.IOException;

public class SaveRoom {

    public static void main( String[] args ) {
	System.out.print("Content-Type: text/xml\r\n");
	String header = "<?xml version='1.0' encoding='utf-8'?>";
	String result = "<result>Saved</result>";
	try {
	    int length = Integer.parseInt(System.getenv("CONTENT_LENGTH"));
	    FileOutputStream save = new FileOutputStream("room-desc.xml");
	    byte[] buf = new byte[1024];
	    int len;
	    while ( (length > 0) && (len=System.in.read(buf)) > 0 ) {
	        save.write( buf, 0, len );
		length -= len;
	    }
	    save.close();
	}
	catch( Exception ex ) {
	    result = "<result>Not saved</result>";
	}
	result = header + result;
	System.out.print( "Content-Length: " + result.length() + "\r\n\r\n" );
	System.out.print( result );
	System.out.flush();
    }
}

Retrieving the arrangement

A simple CGI program that retrieves the arrangement is:

RetrieveRoom.java
import java.io.File;
import java.io.FileInputStream;

public class RetrieveRoom {

    public static void main( String[] args ) {
	System.out.print("Content-Type: text/xml\r\n");
	try {
	    File f = new File("room-desc.xml");
	    FileInputStream retrieve = new FileInputStream( f );
	    byte[] buf = new byte[1024];
	    long length = f.length();
	    System.out.print( "Content-Length: " +  length + "\r\n\r\n" );
	    while ( length > 0 ) {
		int len = retrieve.read( buf );
	        System.out.write( buf, 0, len );
		length -= len;
	    }
	    retrieve.close();
	}
	catch( Exception ex ) {
	    String result = "<result>no room layout</result>";
	    System.out.print( result );
	}
	System.out.flush();
    }
}

Any suggestions?

What problems and improvements can you identify in this application?

Go to the application.