<?xml version="1.0"?>
<!--
 ! http://builder.com.com/5100-6371-1044978.html
 !  - drawLine
 !  - getSVGDocument
 ! http://www.w3.org/Graphics/SVG/
 ! http://www.w3.org/TR/SVG11/
 ! http://jwatt.org/svg/authoring/#namespace-aware-methods
 ! http://wiki.svg.org/Main_Page
 ! http://pilat.free.fr/english/routines/js_dom.htm
 !-->
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:svg="http://www.w3.org/2000/svg" 
  xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  >
  <head>
    <style type="text/css">
      <![CDATA[
      fieldset
      {
        margin: 1pc;
      }

      legend > div
      {
        padding: 2px;
      }

      label
      {
        width: 10em;
        float: left;
        text-align: right;
        margin-right: 0.5em;
        display: block;
      }

      label + input
      {
        margin-left: 0.5em;
      }

      fieldset, legend, input.t
      {
        border: 1px solid blue;
      }

      legend, input.t
      {
        background: wheat;
      }

      #msg
      {
        border: 1px solid;
        background: lavender;
      }
      ]]>
    </style>
    <script>
      <![CDATA[
var SVG_NS = 'http://www.w3.org/2000/svg';
var XLINK_NS = 'http://www.w3.org/1999/xlink';
var msg = null;

function inspect(object)
{
    var properties = '';
    for (prop in object)
        properties += prop + '=' + object[prop] + '\n';
    return properties;
}

// NOTE: I "borrowed" this from prototype-1.4.0/dist/prototype.js
Function.prototype.bindAsEventListener = function(object) {
  var __method = this;
  return function(event) {
    return __method.call(object, event || window.event);
  }
}

function ev_to_svg_xy(ev)
{
    var svg = document.getElementById("svg2");
    var svg_ctm = svg.getScreenCTM();
    var dx = svg_ctm.e;
    var dy = svg_ctm.f;
    return [ev.clientX - dx, ev.clientY - dy];
}

Array.prototype.index_of = function(value)
{
    var i;
    for(i = 0; i < this.length; i += 1)
      { if(value == this[i]) return i; }
    return -1;
}

Array.prototype.remove_value = function(value)
{
    var i;
    var a = new Array();
    for(i = this.length - 1; i >= 0; i -= 1)
      { if(value != this[i]) { a = a.my_append(this[i]); } }
    return a;
}

Array.prototype.to_s = function()
{
    var s = '';
    var i;
    for(i = 0; i < this.length; i += 1)
    {
      s += '[' + i + ']=' + this[i] + '\n';
    }
    return s;
}

Array.prototype.compare_items = function(other)
{
    var i;
    if(other == null || other == undefined) return false;
    if(this.length != other.length) return false;
    for(i = 0; i < this.length; i += 1)
      {
        // NOTE: "undefined" apparently does not inherit from "object"
        //       (thus we have to treat it as a special case from
        //       other objects)
        if(this[i] == null)
          { if(other[i] != null) return false; }
        else if(this[i] == undefined)
          { if(other[i] != undefined) return false; }
        else if(!this[i].compare_items(other[i])) return false;
      }
    return true;
}

Array.prototype.my_append = function(object)
{
    // WARNING: "concat" is no good if "object" happens to be an Array::
    //    a = this.concat(object);
    a = this.concat(null);
    a[this.length] = object;
    return a;
}

Array.prototype.to_pretty_s = function()
{
    var s = '';
    var i;
    s += '[';
    for(i = 0; i < this.length; i += 1)
    {
      if(this[i] == undefined) s += this[i];
      else if(this[i] == null) s += this[i];
      else s += this[i].to_pretty_s();
      if(i < (this.length - 1)) s += ', ';
    }
    s += ']';
    return s;
}

Object.prototype.compare_items = function(other) { return this == other; }
Object.prototype.to_pretty_s = function()
{
    return '' + this;
}

// class CrappyHash {

    function CrappyHash()
    {
        this.keys = new Array();
        this.values = new Array();

        this.index_of = function(key)
        {
            var i;
            for(i = 0; i < this.keys.length; i += 1)
              { if(key.compare_items(this.keys[i])) return i; }
            return -1;
        }

        this.index_of_value = function(value)
        {
            var i;
            for(i = 0; i < this.values.length; i += 1)
              { if(values.compare_items(this.values[i])) return i; }
            return -1;
        }

        this.has_key = function(key) { return this.index_of(key) != -1; }

        this.remove = function(key)
        {
            var i = this.index_of(key);
            if(i != -1)
              {
                delete this.keys[i];
                delete this.values[i];
              }
        }

        this.remove_value = function(value)
        {
            while(true)
              {
                i = this.index_of_value(value);
                if (i == -1) break;
                this.remove(this.keys[i]);
              }
        }

        this.set_key_and_value = function(key, value)
        {
            var i = this.index_of(key);
            if(i == -1)
              {
                this.keys = this.keys.my_append(key);
                this.values = this.values.my_append(value);
              }
            else
              { this.values[i] = value; }
        }

        this.get_value_for_key = function(key)
        {
            var i = this.index_of(key);
            if(i == -1)
              {
                // FIXME: throw an exception here
                return null;
              }
            return this.values[i];
        }
    }

// }

// class NullState {

    function NullState()
    {
        this.ctxt_ = null;
        this.set_context = function(ctxt) { this.ctxt_ = ctxt; }
        this.get_context = function() { return this.ctxt_; }
        // FIXME: should I raise NotImplementedErrors here?
        this.click_vertex = function(vertex) { }
        this.click_edge = function(edge) { }
        this.click_image = function(ev) { }
        this.use_file = function(ev) { }
        this.use_url = function(ev) { }
        this.set_create_vertex = function() { }
        this.set_create_edge = function() { }
        this.set_remove = function() { }
    }

// }

// class StateProxy {

    function StateProxy()
    {
        this.delegate_ = new NullState();
        this.set_delegate = function(delegate)
        {
            this.delegate_ = delegate;
            // FIXME: assert context
            delegate.set_context(this.get_context());
        }
        this.get_delegate = function()
                { return this.delegate_; }
        this.click_vertex = function(vertex)
                { return this.delegate_.click_vertex(vertex) }
        this.click_edge = function(edge)
                { return this.delegate_.click_edge(edge); }
        this.click_image = function(ev)
                { return this.delegate_.click_image(ev); }
        this.use_file = function(ev)
                { this.delegate_.use_file(ev); }
        this.use_url = function(ev)
                { this.delegate_.use_url(ev); }
        this.set_create_vertex = function()
                { this.delegate_.set_create_vertex(); }
        this.set_create_edge = function()
                { this.delegate_.set_create_edge(); }
        this.set_remove = function()
                { this.delegate_.set_remove(); }
    }
    StateProxy.prototype = new NullState();

// }

// class SwitchStateBehavior {

    function SwitchStateBehavior()
    {
        this.set_create_vertex = function()
            { this.get_context().set_state(
                this.get_context().get_state_factory().vertex_state()); }
        this.set_create_edge = function()
            { this.get_context().set_state(
                this.get_context().get_state_factory().edge_state()); }
        this.set_remove = function()
            { this.get_context().set_state(
                this.get_context().get_state_factory().remove_state()); }
    }
    SwitchStateBehavior.prototype = new StateProxy();

// }

// class ClickVertexBehavior {

    function ClickVertexBehavior()
    {
        this.click_vertex = function(vertex)
        {
            this.get_context().shift_src_dest(vertex);
        }
    }
    ClickVertexBehavior.prototype = new StateProxy();

// }

// class CreateVertexState {

    function CreateVertexState()
    {
        this.click_image = function(ev)
        {
            var ctxt = this.get_context();
            var xy = ev_to_svg_xy(ev);
            var v = ctxt.get_widget_factory().vertex(xy[0], xy[1]);
            ctxt.get_graph().append_vertex(v);
            adj.clear();
            adj.writeln(graph_as_adjacency_matrix(ctxt.get_graph()));
            ctxt.set_src_vtx(v);
            ctxt.set_dest_vtx(null);
        }
    }
    CreateVertexState.prototype = new StateProxy();

// }

// class CreateEdgeState {

    function CreateEdgeState()
    {
        var a = new ClickVertexBehavior();
        this.set_delegate(a);
        this.click_image = function(ev)
        {
            var ctxt = this.get_context();
            var xy = ev_to_svg_xy(ev);
            var v = ctxt.get_widget_factory().vertex(xy[0], xy[1]);
            ctxt.get_graph().append_vertex(v);
            adj.clear();
            adj.writeln(graph_as_adjacency_matrix(ctxt.get_graph()));
            // FIXME: more DRY
            //this.get_delegate().click_vertex(v);
            this.get_context().shift_src_dest(v);
            this.try_to_draw_edge();
            // NOTE: end DRY
        }
        this.click_vertex = function(vtx)
        {
            var ctxt = this.get_context();
            var s = ctxt.get_src_vtx();
            var d = ctxt.get_dest_vtx();
            if(s != null && d != null)
              {
                  ctxt.set_src_vtx(null);
                  ctxt.set_dest_vtx(null);
              }
            // FIXME: is routing through the delegate really all that
            //        useful?
            //this.get_delegate().click_vertex(vtx);
            this.get_context().shift_src_dest(vtx);
            this.try_to_draw_edge();
        }
        this.try_to_draw_edge = function()
        {
            var ctxt = this.get_context();
            var s = ctxt.get_src_vtx();
            var d = ctxt.get_dest_vtx();
            if(s != null && d != null)
              {
                var e = ctxt.get_widget_factory().edge(s, d);
                ctxt.get_graph().append_uedge(e, s, d);
                adj.clear();
                adj.writeln(graph_as_adjacency_matrix(ctxt.get_graph()));
              }
        }
    }
    CreateEdgeState.prototype = new StateProxy();

// }

// class RemoveState {
    function RemoveState()
    {
        // FIXME: this code is slightly redundant with that
        //        contained in the erase() methods of Vertex
        //        and Edge.
        // FIXME: this is a real pain in the butt to integrate
        //        with the data layer (ctxt.get_graph())
        this.click_vertex = function(vertex)
          { this.get_context().clear_vertices(); vertex.erase(); }
        this.click_edge = function(edge)
          { this.get_context().clear_vertices(); edge.erase(); }
    }
    RemoveState.prototype = new StateProxy();
// }

// class Context {
    function Context()
    {
        this.state_factory_ = null;
        this.widget_factory_ = null;
        this.src_vtx_ = null;
        this.dest_vtx_ = null;
        this.state_ = null;
        this.graph_ = new MyListBasedGraph();
        this.set_state_factory = function(factory)
          { this.state_factory_ = factory; }
        this.get_state_factory = function()
          { return this.state_factory_; }
        this.set_widget_factory = function(factory)
          { this.widget_factory_ = factory; }
        this.get_widget_factory = function()
          { return this.widget_factory_; }
        this.get_graph = function()
          { return this.graph_; }
        this.set_src_vtx = function(vtx)
        {
            if(this.src_vtx_ == vtx) { return; }
            if(this.src_vtx_ != null) { this.src_vtx_.remove_style_source(); }
            if(vtx != null) { vtx.set_style_source(); }
            this.src_vtx_ = vtx;
        }
        this.get_src_vtx = function(vtx) { return this.src_vtx_; }
        this.set_dest_vtx = function(vtx)
        {
            // FIXME: DRY
            if(this.dest_vtx_ == vtx) { return; }
            if(this.dest_vtx_ != null) { this.dest_vtx_.remove_style_dest(); }
            if(vtx != null) { vtx.set_style_dest(); }
            this.dest_vtx_ = vtx;
        }
        this.get_dest_vtx = function(vtx) { return this.dest_vtx_; }
        this.clear_vertices = function()
        {
            this.set_src_vtx(null);
            this.set_dest_vtx(null);
        }
        this.shift_src_dest = function(vtx)
        {
            if(this.src_vtx_ == null)
              {
                this.set_src_vtx(vtx);
              }
            else if(this.dest_vtx_ == null)
              {
                this.set_dest_vtx(vtx);
              }
            else
              {
                this.set_src_vtx(this.dest_vtx_);
                this.set_dest_vtx(vtx);
              }
        }
        this.get_src = function() { return this.src_vtx_; }
        this.get_dest = function() { return this.dest_vtx_; }
        this.set_state = function(state) { this.state_ = state; }
        this.get_state = function() { return this.state_; }
    }
// }

// class Factory {
    function Factory()
    {
        this.ctxt_ = null;
        // WARNING: set_context must be called before calling 
        //          other factory methods.
        // FIXME: assert ctxt_ == null
        this.set_context = function(ctxt) { this.ctxt_ = ctxt; }
        this.get_context = function() { return this.ctxt_; }
        this.set_context(new Context());
        this.get_context().set_state_factory(this);
        this.get_context().set_widget_factory(this);
        this.set_ctxt_and_delegates = function(for_object)
        {
            // NOTE: SwitchStateBehavior is a "cross cutting concern"
            //       because it adds a level of funcionality that is
            //       shared by most states.  Some people would resort
            //       to Aspect Oriented Programming tools to accomplish
            //       feats like this, but I am using good old design
            //       patterns (proxies/decorators and factories).
            var a = new SwitchStateBehavior();
            //var b = new ClickVertexBehavior();
            //
            for_object.set_context(this.ctxt_);
            //
            for_object.set_delegate(a);
            //a.set_delegate(b);
            return for_object;
        }
        this.vertex_state = function()
        {
            return this.set_ctxt_and_delegates(new CreateVertexState());
        }
        this.edge_state = function()
        {
            return this.set_ctxt_and_delegates(new CreateEdgeState());
        }
        this.remove_state = function()
        {
            return this.set_ctxt_and_delegates(new RemoveState());
        }
        this.get_context().set_state(this.vertex_state());
        //
        this.vertex = function(x, y)
        {
            var ctxt = this.get_context();
            // FIXME: methinks this belongs inside of a factory
            var v = new Vertex();
            v.draw_svg(x, y);
            v.addEventListener("click",
                    function() {
                        ctxt.get_state().click_vertex(v);
                    },
                    false);
            return v;
        }
        this.edge = function(source_vtx, dest_vtx)
        {
            var ctxt = this.get_context();
            var e = new Edge();
            e.connect(source_vtx, dest_vtx);
            e.addEventListener("click",
                    function() {
                        ctxt.get_state().click_edge(e);
                    },
                    false);
            return e;
        }
    }
// }

// class DOM_Log {

    function DOM_Log(parent_node)
    {
        this.parent_node_ = parent_node;
        this.clear = DOM_Log_clear;
        this.writeln = DOM_Log_writeln;
    }

    function DOM_Log_clear()
    {
        while(this.parent_node_.hasChildNodes())
          {
            this.parent_node_.removeChild(this.parent_node_.lastChild);
          }
    }

    function DOM_Log_writeln(msg)
    {
        this.parent_node_.appendChild(document.createTextNode(msg));
        this.parent_node_.appendChild(document.createTextNode('\n'));
    }

// }

// class Vertex {

    var index_dom_object_on_Vertex = new CrappyHash();
    var Vertex_counter = 0;

    function Vertex()
    {
        this.hover_ = false;
        this.style_source_ = false;
        this.style_dest_ = false;
        this.dom_object_ = null;
        this.x_ = null;
        this.y_ = null;
        this.edges_ = new Array();
        this.count_ = Vertex_counter;
        Vertex_counter += 1;
        this.set_dom_object = Vertex_set_dom_object;
        this.get_dom_object = Vertex_get_dom_object;
        this.draw_svg = Vertex_draw_svg;
        this.handle_mouseover = Vertex_handle_mouseover;
        this.handle_mouseout = Vertex_handle_mouseout;
        this.set_style_source = Vertex_set_style_source;
        this.set_style_dest = Vertex_set_style_dest;
        this.remove_style_source = Vertex_remove_style_source;
        this.remove_style_dest = Vertex_remove_style_dest;
        this.update_style = Vertex_update_style;
        this.connects_to = Vertex_connects_to;
        this.remove_edge = Vertex_remove_edge;
        this.erase = Vertex_erase;
        // ...
        this.addEventListener = function(type, callback, capture)
        {
            return this.dom_object_.addEventListener(type, callback, capture);
        }
    }

    function get_Vertex_by_dom_object(dom_object)
    {
        if(!index_dom_object_on_Vertex.has_key(dom_object))
          { return null; }
        x = index_dom_object_on_Vertex.get_value_for_key(dom_object);
        // FIXME: do some assertion on x here
        return x;
    }

    function Vertex_set_dom_object(dom_object)
    {
        if(this.dom_object_ != null)
          { index_dom_object_on_Vertex.remove(this.dom_object_); }
        this.dom_object_ = dom_object;
        index_dom_object_on_Vertex.set_key_and_value(dom_object, this);
    }

    function Vertex_get_dom_object()
    {
        return this.dom_object_;
    }

    // FIXME: should this functionality be moved to the controller layer
    //        or be implemented in a separate view object?
    function Vertex_handle_mouseover(ev)
        { this.hover_ = true; this.update_style(); }

    function Vertex_handle_mouseout(ev)
        { this.hover_ = false; this.update_style(); }

    function Vertex_draw_svg(x, y)
    {
        this.x_ = x;
        this.y_ = y;
        var circle = document.createElementNS(SVG_NS, "circle");
        circle.setAttributeNS(null, "cx", x);
        circle.setAttributeNS(null, "cy", y);
        circle.setAttribute("r", "5");
        circle.addEventListener("mouseover",
                        this.handle_mouseover.bindAsEventListener(this),
                        false);
        circle.addEventListener("mouseout",
                        this.handle_mouseout.bindAsEventListener(this),
                        false);
        document.getElementById("svg_vertices").appendChild(circle);
        this.set_dom_object(circle);
    }

    function Vertex_set_style_source()
        { this.style_source_ = true; this.update_style(); }

    function Vertex_set_style_dest()
        { this.style_dest_ = true; this.update_style(); }

    function Vertex_remove_style_source()
        { this.style_source_ = false; this.update_style(); }

    function Vertex_remove_style_dest()
        { this.style_dest_ = false; this.update_style(); }

    function Vertex_update_style()
    {
        if(this.hover_)
          {
            var i;
            this.get_dom_object().setAttribute("fill", "cyan");
            for(i = 0; i < this.edges_.length; i += 1)
              {
                // FIXME: this is not right.  Some other vertex
                //        might call "remove_style" whilst this
                //        one is calling "set_style."
                this.edges_[i].set_style_vertex_highlight();
              }
          }
        else
          {
            for(i = 0; i < this.edges_.length; i += 1)
              {
                this.edges_[i].remove_style_vertex_highlight();
              }
            // FIXME: need a "will be used as source for next click"
            //        style rather than a "was used as source for
            //        last click" style
            if(this.style_source_ || this.style_dest_)
                this.get_dom_object().setAttribute("fill", "green");
            else
                this.get_dom_object().setAttribute("fill", "black");
          }
    }

    function Vertex_connects_to(edge)
    {
        this.edges_ = this.edges_.my_append(edge);
        this.update_style();
    }

    function Vertex_remove_edge(edge)
    {
        this.edges_ = this.edges_.remove_value(edge);
    }

    function Vertex_erase()
    {
      var i;
      // WARNING: this loop requires robust iteration (because erasing an
      //          edge modifies this.edges_)
      for(i = this.edges_.length - 1; i >= 0; i -= 1)
        {
          this.edges_[i].erase();
        }
      document.getElementById("svg_vertices").
          removeChild(this.get_dom_object());
      this.set_dom_object(null);
    }

// }

// class Graph {

    function Graph()
    {
        // manipulation methods:
        this.append_vertex = function(vtx) { }
        this.remove_vertex = function(vtx) { }
        this.append_uedge = function(edge, vtx1, vtx2) { }
        this.append_dedge = function(edge, svtx, dvtx) { }
        this.remove_edge = function(edge) { }

        // accessor methods:
        this.vertices = function() { }
        this.dedges = function() { }
        this.uedges = function() { }
        this.sedges_for_vertex = function(svtx) { }
        this.dedges_for_vertex = function(dvtx) { }
        this.uedges_for_vertex = function(vtx) { }
        this.uedge_for_vertices = function(vtx1, vtx2) { }
        this.svertex_for_dedge = function(dedge) { }
        this.dvertex_for_dedge = function(dedge) { }
        this.vertices_for_uedge = function(uedge) { }

        // mixins (easy accessors):
        this.vertices_for_edge = function(edge) { }
        this.edges_for_vertex = function(vtx) { }
    }

    function MyListBasedGraph()
    {
        this._vertices = new Array();
        this._uedges = new CrappyHash();
        this.append_vertex = function(vtx)
        {
            // FIXME: assert(this._vertices.index_of(vtx) == -1);
            this._vertices = this._vertices.my_append(vtx);
        }
        this.remove_vertex = function(vtx)
        {
            this._vertices = this._vertices.remove_value(vtx);
        }
        this.append_uedge = function(edge, vtx1, vtx2)
        {
            this._uedges.set_key_and_value([vtx1, vtx2], edge);
            this._uedges.set_key_and_value([vtx2, vtx1], edge);
        }
        this.remove_edge = function(edge)
        {
            this._uedges.remove_value(edge);
        }
        this.vertices = function()
        {
            return this._vertices;
        }
        this.uedge_for_vertices = function(vtx1, vtx2)
        {
            return this._uedges.get_value_for_key([vtx1, vtx2]);
        }
    }
    MyListBasedGraph.prototype = new Graph();

    function graph_as_adjacency_matrix(graph)
    {
        s = '';
        for(i = 0; i < graph.vertices().length; i += 1)
          {
            v1 = graph.vertices()[i];
            for(j = 0; j < graph.vertices().length; j += 1)
              {
                v2 = graph.vertices()[j];
                e = graph.uedge_for_vertices(v1, v2);
                if(e == null) s += '0 ';
                else s += '1 ';
              }
            s += '\n'
          }
          return s;
    }

// }

// class Edge {
    function Edge()
    {
        this.src_vtx_ = null;
        this.dest_vtx_ = null;
        this.dom_edge_ = null;
        this.style_vertex_highlight_ = false;
        this.set_dom_object = function(foo) { this.dom_edge_ = foo; }
        this.get_dom_object = function() { return this.dom_edge_; }
        this.connect = function(src, dest)
        {
            this.src_vtx_ = src;
            this.dest_vtx_ = dest;
            src.connects_to(this);
            dest.connects_to(this);
            this.draw();
        }
        this.set_style_vertex_highlight_ = false;
        this.set_style_vertex_highlight = function()
          { this.style_vertex_highlight_ = true; this.update_style(); }
        this.remove_style_vertex_highlight = function()
          { this.style_vertex_highlight_ = false; this.update_style(); }
        this.update_style = function()
        {
            if(this.get_dom_object() != null)
              {
                if(this.style_vertex_highlight_)
                  {
                    this.get_dom_object().setAttribute("stroke", "dodgerblue");
                  }
                else
                  {
                    this.get_dom_object().setAttribute("stroke", "black");
                  }
              }
        }
        this.handle_mouseover = function(ev)
        {
            this.get_dom_object().setAttribute("stroke", "cyan");
        }
        this.handle_mouseout = function(ev)
        {
            this.get_dom_object().setAttribute("stroke", "black");
        }
        this.draw = function()
        {
            if(this.get_dom_object() == null)
              {
                var edge = document.createElementNS(SVG_NS, "line");
                edge.setAttribute("stroke", "black");
                edge.setAttribute("stroke-width", "5");
                edge.addEventListener("mouseover",
                                this.handle_mouseover.bindAsEventListener(this),
                                false);
                edge.addEventListener("mouseout",
                                this.handle_mouseout.bindAsEventListener(this),
                                false);
                edge.setAttributeNS(null, "x1", "100");
                edge.setAttributeNS(null, "y1", "100");
                edge.setAttributeNS(null, "x2", "200");
                edge.setAttributeNS(null, "y2", "200");
                document.getElementById("svg_edges").appendChild(edge);
                this.set_dom_object(edge);
              }
            var x1 = this.src_vtx_.x_; // FIXME
            var y1 = this.src_vtx_.y_;
            var x2 = this.dest_vtx_.x_;
            var y2 = this.dest_vtx_.y_;
            this.get_dom_object().setAttributeNS(null, "x1", x1);
            this.get_dom_object().setAttributeNS(null, "y1", y1);
            this.get_dom_object().setAttributeNS(null, "x2", x2);
            this.get_dom_object().setAttributeNS(null, "y2", y2);
            this.update_style();
        }
        this.erase = function()
        {
            this.src_vtx_.remove_edge(this);
            this.dest_vtx_.remove_edge(this);
            document.getElementById("svg_edges").
                removeChild(this.get_dom_object());
        }
        this.addEventListener = function(type, callback, capture)
        {
            return this.get_dom_object().addEventListener(type, callback, capture);
        }
    }
// }

function html_show_id(id)
{
    document.getElementById(id).style['display'] = 'block';
}

function html_hide_id(id)
{
    document.getElementById(id).style['display'] = 'none';
}

function show_click_msg(id)
{
    //txt = document.createTextNode("&#8701; Click the button");
    //txt = document.createEntityReference("#8701");
    //txt = document.createTextNode("\u21FD Click the button");
    var txt = document.createTextNode("&larr; Click the button");
    replace_contents(document.getElementById(id), txt);
}

function hide_click_msg(id)
{
    var txt = document.createTextNode("");
    replace_contents(document.getElementById(id), txt);
}

function handle_change_file(ev)
{
    show_click_msg('clickme.url');
}

function handle_change_url(ev)
{
    hide_click_msg('clickme.url');
}

function handle_load_image_file(ev)
{
    replace_contents(document.getElementById("msg"),
                     document.createTextNode(inspect(ev)));
}

function handle_load_image_url(ev)
{
    var url = document.getElementById('url').value;
    document.getElementById("underlay_image").
        setAttributeNS(
            XLINK_NS,
            'xlink:href',
            url
        );
    //replace_contents(document.getElementById("msg"),
    //                 document.createTextNode(url));
}

function my_assert(fact, msg)
{
    // NOTE: Since JavaScript consoles probably don't give a stack dump
    //       (i.e. so you can see which function called which), the
    //       work-around is to write useful msg's to help you figure out
    //       which part of the code failed assertion.
    if(!fact) throw new Error(msg);
}

function regression()
{
    var verbose = 1;
    var i;
    var id;

    var list = new Array();
    id = 'list.my_append '; i = 0;
    list = list.my_append([1, 2]);
    list = list.my_append([3, 4]);
    list = list.my_append(5);
    my_assert(list[0][1] == 2, id + (i++));
    my_assert(list[1][0] == 3, id + (i++));
    my_assert(list[2] == 5, id + (i++));

    id = 'Array.compare_items '; i = 0;
    my_assert([1, 3].compare_items([1, 3]), id + (i++));
    my_assert([1, [3, 4]].compare_items([1, [3, 4]]), id + (i++));
    my_assert([1, [3, 4]] != [1, [3, 4]], id + (i++));

    var hash = new CrappyHash();
    id = 'CrappyHash1 '; i = 0;
    hash.set_key_and_value(1, 2);
    hash.set_key_and_value(3, 4);
    if(verbose) msg.writeln(1);
    my_assert(hash.get_value_for_key(1) == 2, id + (i++));
    my_assert(hash.get_value_for_key(3) == 4, id + (i++));
    my_assert(hash.get_value_for_key(5) == null, id + (i++));

    var hash2 = new CrappyHash();
    id = 'CrappyHash2 '; i = 0;
    hash.set_key_and_value(['a', 'b'], 2);
    hash.set_key_and_value(['c', 'd'], 4);
    my_assert(hash.get_value_for_key(['a', 'b']) == 2, id + (i++));
    my_assert(hash.get_value_for_key(['c', 'd']) == 4, id + (i++));
    my_assert(hash.get_value_for_key(['e', 'f']) == null, id + (i++));

    var graph1 = new MyListBasedGraph();
    var o1 = new Object();
    var o2 = new Object();
    id = 'MyListBasedGraph1 '; i = 0;
    my_assert(o1 == o1, id + (i++));
    my_assert(o1 != o2, id + (i++));
    graph1.append_uedge(42, 3, 4);
    graph1.append_uedge(1, o1, o2);
    graph1.append_uedge(2, o2, o2);
    my_assert(graph1.uedge_for_vertices(3, 4) == 42, id + (i++));
    my_assert(graph1.uedge_for_vertices(4, 3) == 42, id + (i++));
    my_assert(graph1.uedge_for_vertices(o1, o2) == 1, id + (i++));
    my_assert(graph1.uedge_for_vertices(o2, o1) == 1, id + (i++));
    my_assert(graph1.uedge_for_vertices(o2, o2) == 2, id + (i++));
}

function init()
{
    // See also:
    // - http://en.wikipedia.org/wiki/DOM_Events
    // NOTE: msg is a global variable (don't prefix it with "var ")
    msg = new DOM_Log(document.getElementById('msg'));
    adj = new DOM_Log(document.getElementById('adj'));
    var img = document.getElementById("underlay_image");

    regression();

    var factory = new Factory();
    var context = factory.get_context();

    var click_image = function(ev)
    {
        // FIXME: this ev.target == img constraint might be a little
        //        too finicky.
        if(ev.target == img) context.get_state().click_image(ev);
    }
    var place_vertex = function(ev)
    {
        context.get_state().set_create_vertex();
    }
    place_edge = function(ev)
    {
        context.get_state().set_create_edge();
    }
    var remove = function(ev)
    {
        context.get_state().set_remove();
    }

    img.addEventListener("click", click_image, false);
    document.getElementById("loadimage.file").
      addEventListener("click", handle_load_image_file, false);
    document.getElementById("loadimage.url").
      addEventListener("click", handle_load_image_url, false);
    document.getElementById("file").
      addEventListener("change", handle_change_file, false);
    document.getElementById("url").
      addEventListener("change", handle_change_url, false);
    document.getElementById("i_place_vertex").
      addEventListener("change", place_vertex, false);
    document.getElementById("i_connect_vertices").
      addEventListener("change", place_edge, false);
    document.getElementById("i_remove").
      addEventListener("change", remove, false);
    msg.writeln('Messages:');

    //msg.writeln(str([]));
}
      ]]>
    </script>
    <title>Graph Overlay Drawing Program</title>
  </head>
  <body onload="init();">
    <h1>Graph Overlay Drawing Program</h1>

    <h2>Prerequisites</h2>

    <div>
      This program requires one of the following web browsers:
      <!-- FIXME: autodetect this -->
      <ul>
        <li>&gt;= Mozilla Firefox 1.5</li>
      </ul>
    </div>

    <pre id="msg"/>

    <pre id="adj"/>

    <div>
      <form action="#">
        <fieldset>
          <legend>
            <div>Specify an image to use:</div>
          </legend>
            <p>
              <label for="file">
                Use a file on your computer:
              </label>
              <input id="file" name="file" class="t"
                     type="file" size="40"/>
              <input id="loadimage.file" name="loadimage.file"
                     type="button" value="Use this File"/>
              <span id="clickme.file"/>
            </p>
            <p>
              <label for="url">
                Specify a URL of an image to use:
              </label>
              <input id="url" name="url" class="t"
                     type="text" size="40"/>
              <input id="loadimage.url" name="loadimage.url"
                     type="button" value="Use this URL"/>
              <span id="clickme.url"/>
            </p>
        </fieldset>
      </form>
    </div>


    <div style="float: left; border: thin silver solid; width: 500px; height: 500px;">
      <svg:svg id="svg1">
        <svg:g id="svg2">
          <svg:g id="svg_underlay">
            <svg:image id="underlay_image"
              xlink:href="http://www.google.com/intl/en/images/logo.gif"
              width="500" height="500"/>
          </svg:g>
          <svg:g id="svg_edges">
          </svg:g>
          <svg:g id="svg_vertices">
          </svg:g>
        </svg:g>
      </svg:svg>
    </div>

    <div style="float: left;">
      <form action="#">
        <fieldset>
          <legend>
            <div>Choose a tool:</div>
          </legend>
          <p>
            <input id="i_place_vertex"
                   name="tool" type="radio" size="40" value="true"/>
            Place and move vertices
            <br/>
            <input id="i_connect_vertices"
                   name="tool" type="radio" size="40" value=""/>
            Place and connect vertices
            <br/>
            <input id="i_remove"
                   name="tool" type="radio" size="40" value=""/>
            Remove edges and vertices
          </p>
        </fieldset>
      </form>
    </div>

  </body>
</html>

