SVG Building Map Sample 01Due to blog restrictions, the sample is located on a separate page.

building.html: SVG sample

BuildingSample01.zip: source (6KB)

Note: In my previous blog entry, I stated that SVG is a technology with diminishing support and that ultimately I plan to convert this building map to SilverLight.

The first example is a very basic building map. I created a simple SVG file to represent a building and then created occupants for the building (as if you could keep these crazy rascals in a building -- look at the names and you should know what I mean).

For simplicity this sample does not dynamically load the occupant information nor does it have multiple floors. Instead the occupants are specified in a static JavaScript file although this is identical to the Adobe sample where the occupants are static. Future enhancements will include these features as well as additional functionality.

Step 1: Define the Building Map

I used Inkscape to create the SVG image, but ultimately ended up changing the SVG/XML by hand to create the simplest case for this example.

Step 2: Define a "Room"

This example will only make use of a "room" although future examples will have the concept of a "resource" (printer, conference room, etc.). In order to handle the JavaScript events (well technically ecmascript events), the script handlers need to be able to identify a room and so I simply used added the class "room" to the SVG element:

    <style type="text/css">
/* snip, snip... */
.room {
    fill:#c0c0ff;
    stroke:#222277;
    stroke-width:1;
}
/* snip, snip... */
    </style>
    <!-- more snip, snip... -->
    <g
        id="room101"
        class="room"
    >
        <rect
            id="rect101"
            class="room"
            width="150"
            height="100"
            x="0"
            y="0"
        />
        <text
            id="text101"
            x="50"
            y="55"
            class="label"
            xml:space="preserve"
        >
            <tspan
                x="50"
                y="55"
                id="tspan101"
            >101</tspan>
        </text>
    </g>

Notice that the room is defined as a group: { rect, text { tspan } }. The JavaScript handler will assume this definition will be consistent for all rooms.

Step 3: Add the Room handler to JavaScript

The JavaScript handler identifies a "room" to avoid processing elements that are "rooms." The line that contains: if (className != "room") will prevent the script from processing non-rooms.

// get room name and ID
///////////////////////
function getRoom(target)
{
    var svgElem = getNodeOrParent(target, "g");
    if (svgElem)
    {
        var thisRm = svgElem;
        var thisType = thisRm.tagName;
        var id = thisRm.getAttribute('id');
        var className = thisRm.getAttribute('class');
        if (className != "room")
            return;

        var num;
        var rect = getChildElem(svgElem, "rect");
        var text = getChildElem(svgElem, "tspan");
        if (text)
            num = text.firstChild.data;

        return { 'num':num, 'id':id, 'rect':rect, 'group':svgElem };
    }
}

Step 4: Handle the mouse events

The base group specifies the event handlers for the entire SVG.

    <g
        id="room-layer"
        onclick="selector(evt);"
        onmouseover="onImg(evt);"
        onmouseout="offImg(evt);"
    >

The local SVG functions are just references to JavaScript functions in the hosted page:

<script type="text/ecmascript"><![CDATA[
function onImg(evt)
{
    if (top && top.onImg)
        top.onImg(evt);
}

function offImg(evt)
{
    if (top && top.offImg)
        top.offImg(evt);
}

function selector(evt)
{
    if (top && top.selector)
        top.selector(evt);
}
]]></script>

Step 5: Handle the onmouseover event

The mouseover event displays the occupant information. If the user has clicked on a room and selected that room, the mouseover event will not update the display information.

// room mouseovers
function onImg(mouseEvt)
{
    // No highlighting when an item is selected
    if (selection && selection.room)
        return;

    // get the room name and ID
    var target = mouseTarget(mouseEvt);
    var room = getRoom(target);
    
    if (room &&
        highlighted && 
        highlighted.room &&
        highlighted.room.id == room.id)
    {
        // Same item as before highlighted
        return;
    }
    
    // save the style object for the rolloff
    var oldStyle = updateStyle(room, rollStyles.rollOver);
    // show the occupant info
    displayRoom(room);
    highlighted = { room: room, oldStyle: oldStyle } ;
}

Step 6: Handle the onmouseout event

When the user moves the mouse off of a room, the display will clear the occupant information. If a user is selected, the display will keep the selected occupant.

function offImg(mouseEvt)
{
    if (highlighted)
    {
        resetStyle(highlighted);
        highlighted = null;
    }
}

Step 7: Handle the room selection (onclick event)

When the user clicks on a room, the page will select that room and stop the automatic display of other information during mouseover and mouseout events.

function selector(mouseEvt) {
    // Turn off any highlighting
    offImg(mouseEvt);

    // get the room name and ID
    var target = mouseTarget(mouseEvt);
    var room = getRoom(target);

    if (room &&
        selection && 
        selection.room &&
        room.id == selection.room.id)
    {
        // Deselect the item
        resetStyle(selection);
        selection = null;
        return;
    }

    resetStyle(selection);
    // save the style object for the rolloff
    var oldStyle = updateStyle(room, rollStyles.selected);

    // show the occupant info
    displayRoom(room);
    selection = { room: room, oldStyle: oldStyle } ;
}

Step 8: Handle the style changes to the SVG elements

A lot of the challenge to get this sample to work in FireFox was with the setProperty function. When I added the last "null" parameter, it started working.

var rollStyles = {
    rollOver: {
        fill            : 'cyan'
       ,"stroke-width"  : '3' }
   ,selected: {
        fill            : 'blue'
       ,"stroke-width"  : '3' }
};
/* snip, snip... */
function updateStyle(elem, styleType) {
    var svgElem = elem ? elem.rect: null;
    if (svgElem && styleType)
    {
        var svgStyle = svgElem.style;
        var oldStyle = { };
        for (var name in styleType) {
            oldStyle[name] = svgStyle.getPropertyValue(name);
            try {
            svgStyle.setProperty(name, styleType[name], null);
            } catch (e) {
                window.alert('Error ' + e.number + ": " + e.message);
            }
        }
        elem["oldStyle"] = oldStyle;
        return oldStyle;
    }
}

function resetStyle(elem) 
{
    var svgElem = (elem && elem.room) ? elem.room.rect : null;
    var oldStyle = elem ? elem.oldStyle : null;
    if (svgElem && oldStyle)
    {
        var svgStyle = svgElem.style;
        for (var name in oldStyle) 
        {
            if ( !oldStyle[name] )
                svgStyle.removeProperty(name);
            else
                svgStyle.setProperty(name, oldStyle[name], null);
        }
    }
}

Step 9: Display the occupant information

When the onmouseover event fires on a room, the page displays the occupant information:

// display room info
function displayRoom(room)
{
    var found = false;
    if (room && room.num)
    {
        var id = "Rm_" + room.num;
        if (g_rmData && g_rmData[id]) 
        {
            var person1 = g_rmData[id];
            document.getElementById("tdRoom").innerHTML = person1.room;
            document.getElementById("tdName").innerHTML = person1.name;
            document.getElementById("tdPhone").innerHTML = person1.phone;
            document.getElementById("tdEmail").innerHTML = "<a href='mailto:" + person1.email + "'>" + person1.email + "</a>";
            found = true;
        }
    }
    if (!found) 
    {
            document.getElementById("tdRoom").innerHTML = "&nbsp;";
            document.getElementById("tdName").innerHTML = "&nbsp;";
            document.getElementById("tdPhone").innerHTML = "&nbsp;";
            document.getElementById("tdEmail").innerHTML = "&nbsp;";
    }
}

Step 10: Define the occupant information

For this sample, the data is defined in a static javascript file.

var g_rmRawData = [
 { room : '100', name : 'Bugs Bunny', phone : '1-800-555-0100', email : 'bugs.bunny@looneytunes.null' }
,{ room : '101', name : 'Marvin the Martian', phone : '1-800-555-0101', email : 'marvin.the.martian@looneytunes.null' }
,{ room : '102', name : 'Tweety', phone : '1-800-555-0102', email : 'tweety@looneytunes.null' }
,{ room : '103', name : 'Taz', phone : '1-800-555-0103', email : 'taz@looneytunes.null' }
,{ room : '104', name : 'Daffy Duck', phone : '1-800-555-0104', email : 'daffy.duck@looneytunes.null' }
,{ room : '105', name : 'Foghorn Leghorn', phone : '1-800-555-0105', email : 'foghorn.leghorn@looneytunes.null' }
,{ room : '106', name : 'Miss Prissy', phone : '1-800-555-0106', email : 'miss.prissy@looneytunes.null' }
,{ room : '107', name : 'Chickenhawk', phone : '1-800-555-0107', email : 'chickenhawk@looneytunes.null' }
,{ room : '108', name : 'Dawg', phone : '1-800-555-0108', email : 'dawg@looneytunes.null' }
,{ room : '109', name : 'Wile E Coyote', phone : '1-800-555-0109', email : 'wile.e.coyote@looneytunes.null' }
,{ room : '110', name : 'Road Runner', phone : '1-800-555-0110', email : 'road.runner@looneytunes.null' }
,{ room : '111', name : 'Speedy Gonzalez', phone : '1-800-555-0111', email : 'speedy.gonzalez@looneytunes.null' }
,{ room : '112', name : 'Pepe Le Pew', phone : '1-800-555-0112', email : 'pepe.le.pew@looneytunes.null' }
,{ room : '113', name : 'Penelope', phone : '1-800-555-0113', email : 'penelope@looneytunes.null' }
,{ room : '114', name : 'Porky Pig', phone : '1-800-555-0114', email : 'porky.pig@looneytunes.null' }
,{ room : '115', name : 'Elmer Fudd', phone : '1-800-555-0115', email : 'elmer.fudd@looneytunes.null' }
];

var g_rmData = [];

function loadRoomData()
{
    for (var i = 0; i < g_rmRawData.length; ++i)
        g_rmData['Rm_' + g_rmRawData[i].room] = g_rmRawData[i];
}

loadRoomData();

For those who are interested, I copied the names of the characters from the Looney Tunes site and ran this Python script:

def makeemail(s):
    return s.lower().replace(' ','.') + '@looneytunes.null'
    
def makedict(name, num):
    return { 'name':name, 'room':str(num), 'email':makeemail(name) }
    
def makedata(names, num, f):
    for name in names:
        print f % makedict(name, num)
        num = num + 1

n = ["Bugs Bunny",
    "Marvin the Martian",
    "Tweety",
    "Taz",
    "Daffy Duck",
    "Foghorn Leghorn",
    "Miss Prissy",
    "Chickenhawk",
    "Dawg",
    "Wile E Coyote",
    "Road Runner",
    "Speedy Gonzalez",
    "Pepe Le Pew",
    "Penelope",
    "Porky Pig",
    "Elmer Fudd",
]

s = "{ room : '%(room)s', name : '%(name)s', phone : '1-800-555-0%(room)s', email : '%(email)s' }"

makedata(n, 100, s)

Conclusion

This sample demonstrates the simplest case with static data and a very simple building map with a small number of occupants. Part of the challenge I had when I first analyzed the Adobe Visual Building Search sample was the complexity and questionable coding. Hopefully this sample is much simpler.

Part of the challenge in developing for the web is the differences in the browsers -- occasionally it is because one browser will let you get away with something you shouldn't be able to do. That was the case with this example -- I spent a little extra time making it work with FireFox. The eventing model and JavaScript was a little less forgiving in FireFox than IE.

kick it

Technorati tags: ,