/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is SVG Snap-To-Grid Example.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

if (typeof SVG_NS != "string") {
  const SVG_NS = "http://www.w3.org/2000/svg";
}

const SnapGrid = {
/******************************************************************************
 * Private properties for SnapGrid.                                           *
 ******************************************************************************/

  // The grid element this control associates with.
  _gridElement: document.getElementById("snapgrid"),

  // The transaction manager this control uses.  May be null.
  _txmgr: null,

  // The SVG element we're editing.  DO NOT SET HERE.  Use attach().
  _editee: null,

  // A map of important elements this widget uses internally.
  _elementMap: null,

  // The current editing mode of the snap grid.
  _mode: "uninitialized",

  // True if we are currently drawing a SVG path.
  _isDrawingPath: false,

  // The current SnapObjectClass instance.  This must never be a SnapPoint.
  _currentSnapObject: null,

  // The current SnapPoint instance which the user is manipulating.
  _currentSnapPoint: null,

  /**
   * Add an event listener to translate events into SnapGrid commands.
   *
   * @param aCommandType The command type to dispatch.
   * @param aNode        The DOM node to add the event listener to.
   * @param aEventType   The type of event to listen for.
   */
  _addCommandHandler:
  function addCommandHandler(aCommandType, aNode, aEventName) {
    aNode.addEventListener(aEventName, {
      // nsIDOMEventListener
      handleEvent: function handleEvent(aEvent) {
        SnapGrid.commands[aCommandType](aEvent);
      }
    }, true);
  },

  /**
   * A special DOM event listener for keystrokes.
   */
  _keyHandler: {
    /**
     * A map of DOM key codes to command types.
     */
    keyMap: {},

    // nsIDOMEventListener
    handleEvent: function handleEvent(aEvent) {
      if (aEvent.keyCode in this.keyMap) {
        var commandType = this.keyMap[aEvent.keyCode];
        SnapGrid.commands[commandType](aEvent);
      }
    }
  },

  /**
   * Bind a key to a SnapGrid command type.
   *
   * @param aCommandType The command type to dispatch.
   * @param aKeyCode     The DOM KeyEvent key code to bind the command to.
   */
  _addKeyCommandHandler: function addKeyHandler(aCommandType, aKeyCode) {
    this._keyHandler.keyMap[aKeyCode] = aCommandType;
  },

/******************************************************************************
 * Public properties for SnapGrid.                                            *
 ******************************************************************************/

  /**
   * Detach the current SnapObject.
   */
  detachCurrentSnapObject: function detachCurrentSnapObject() {
    switch (this._mode) {
      case "uninitialized":
      case "select":
        throw Components.results.NS_ERROR_ABORT;

      case "line":
        break;

      default:
        throw Components.results.NS_ERROR_UNEXPECTED;
    }

    if (this._currentSnapObject) {
      this._currentSnapObject.detach();
      this._currentSnapObject = null;
    } else {
      dump("SnapGrid.detachCurrentSnapObject:  No current snap object?\n\n");
    }

    if (this._currentSnapPoint) {
      this._currentSnapPoint.detach();
      this._currentSnapPoint = null;
    } else {
      dump("SnapGrid.detachCurrentSnapPoint:  No current snap point?\n\n");
    }
  },

  // Command methods for the snap grid.
  commands: {
    /**
     * Clear the selection.
     */
    clearSelection: function clearSelection(aEvent) {
      if (SnapGrid._isDrawingPath) {
        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
      } else {
        switch (SnapGrid._mode) {
          case "select":
            // do nothing
            break;

          case "line":
            var gridControlsLine = SnapGrid._elementMap.gridControlsLine;
            if (gridControlsLine.getAttribute("selected") == "true") {
              this.toggleLineDrawMode(aEvent);
            } else {
              throw Components.results.NS_ERROR_UNEXPECTED;
            }
            break;
          default:
            throw Components.results.NS_ERROR_UNEXPECTED;
        }
      }

      SnapGrid._mode = "select";
      SnapGrid._isDrawingPath = false;
    },

    /**
     * Accept the selection and move on to the next step.
     */
    acceptSelection: function acceptSelection(aEvent) {
      if (SnapGrid._isDrawingPath) {
        throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
      } else {
        switch (SnapGrid._mode) {
          case "select":
            // do nothing
            break;

          case "line":
            lineObj = SnapGrid._currentSnapObject;
            if (!(lineObj instanceof SnapLine)) {
              throw Components.results.NS_ERROR_UNEXPECTED;
            }
            lastPoint = SnapGrid._currentSnapPoint;
            var point = lineObj.initializeBySteps(lastPoint.x + 1, lastPoint.y + 1);
            if (point) {
              point.select();
              SnapGrid._elementMap.pointsSet.appendChild(point._element);
              SnapGrid._currentSnapPoint = point;
            } else {
              this.clearSelection();
            }
        }
      }

      if (!SnapGrid._currentSnapObject) {
        if (SnapGrid._txmgr) {
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        } else {
          var range = document.createRange();
          range.selectNodeContents(SnapGrid._elementMap.enlargedItem);
          var frag = range.cloneContents();

          range.selectNodeContents(SnapGrid._editee);
          range.deleteContents();
          range.detach();
          range = null;
          delete range;

          SnapGrid._editee.appendChild(frag);
        }
      }
    },

    /**
     * Move the selection up by one pixel.
     */
    moveUp: function moveUp(aEvent) {
      if ((SnapGrid._mode == "uninitialized") ||
          (SnapGrid._mode == "select")) {
        return;
      }
      if (SnapGrid._currentSnapPoint) {
        SnapGrid._currentSnapPoint.moveUp();
      } else if (SnapGrid._currentSnapObject) {
        SnapGrid._currentSnapObject.moveUp();
      } else {
        throw Components.results.NS_ERROR_UNEXPECTED;
      }
    },

    /**
     * Move the selection right by one pixel.
     */
    moveRight: function moveRight(aEvent) {
      if ((SnapGrid._mode == "uninitialized") ||
          (SnapGrid._mode == "select")) {
        return;
      }
      if (SnapGrid._currentSnapPoint) {
        SnapGrid._currentSnapPoint.moveRight();
      } else if (SnapGrid._currentSnapObject) {
        SnapGrid._currentSnapObject.moveRight();
      } else {
        throw Components.results.NS_ERROR_UNEXPECTED;
      }
    },

    /**
     * Move the selection down by one pixel.
     */
    moveDown: function moveDown(aEvent) {
      if ((SnapGrid._mode == "uninitialized") ||
          (SnapGrid._mode == "select")) {
        return;
      }
      if (SnapGrid._currentSnapPoint) {
        SnapGrid._currentSnapPoint.moveDown();
      } else if (SnapGrid._currentSnapObject) {
        SnapGrid._currentSnapObject.moveDown();
      } else {
        throw Components.results.NS_ERROR_UNEXPECTED;
      }
    },

    /**
     * Move the selection left by one pixel.
     */
    moveLeft: function moveLeft(aEvent) {
      if ((SnapGrid._mode == "uninitialized") ||
          (SnapGrid._mode == "select")) {
        return;
      }
      if (SnapGrid._currentSnapPoint) {
        SnapGrid._currentSnapPoint.moveLeft();
      } else if (SnapGrid._currentSnapObject) {
        SnapGrid._currentSnapObject.moveLeft();
      } else {
        throw Components.results.NS_ERROR_UNEXPECTED;
      }
    },

    /**
     * Move the selection according to the mouse coordinates.
     */
    moveMouse: function moveMouse(aEvent) {
      if ((SnapGrid._mode == "uninitialized") ||
          (SnapGrid._mode == "select")) {
        return;
      }
      with (SnapGrid._elementMap) {
        var matrix = rectClick.getCTM();
        matrix = matrix.inverse();
        matrix = matrix.translate(aEvent.clientX, aEvent.clientY);
        var x = Math.round(matrix.e);
        var y = Math.round(matrix.f);
        msg = "(" + x + ", " + y + ")";

        if (SnapGrid._currentSnapPoint) {
          SnapGrid._currentSnapPoint.moveTo(x, y);
        } else if (SnapGrid._currentSnapObject) {
          SnapGrid._currentSnapObject.moveTo(x, y);
        } else {
          throw Components.results.NS_ERROR_UNEXPECTED;
        }

        textOutput.firstChild.nodeValue = msg;
      }
    },

    /**
     * Mouse out from rectClick.  Usually means hiding the selection.
     */
    rectClickMouseOut: function rectClickMouseOut(aEvent) {
      if (SnapGrid._currentSnapPoint) {
        SnapGrid._currentSnapPoint.hide();
      }
    },

    /**
     * Toggle the line drawing mode.
     *
     * @param aCommandType The type of command to execute.
     * @param aEvent       A DOM event which triggered the command.
     */
    toggleLineDrawMode: function toggleLineDrawMode(aEvent) {
      var gridControlsLine = SnapGrid._elementMap.gridControlsLine;
      if (gridControlsLine.getAttribute("selected") == "true") {
        gridControlsLine.removeAttribute("selected");
        SnapGrid.detachCurrentSnapObject();
      } else {
        gridControlsLine.setAttribute("selected", "true");

        var lineObj = new SnapLine();
        var newPoint = lineObj.initializeBySteps(15, 15);
        newPoint.select();
        SnapGrid._elementMap.pointsSet.appendChild(newPoint._element);
        SnapGrid._currentSnapPoint = newPoint;
        SnapGrid._currentSnapObject = lineObj;
        SnapGrid._mode = "line";
      }
    }
  },

  /**
   * Check to see if we're initialized.
   */
  checkInitialized: function checkInitialized() {
    if (this._mode != "uninitialized") {
      return;
    }
    throw Components.results.NS_ERROR_NOT_INITIALIZED;
  },

  /**
   * Detach this widget from the SVG element we're currently editing.
   */
  detach: function detach() {
    this._checkInitialized();

    with (this._elementMap) {
      var range = document.createRange();
      range.selectNodeContents(enlargedItem);
      range.deleteContents();
      range.detach();
      range = null;
      delete range;
    }

    window.removeEventListener("keydown", this._keyHandler, true);

    this._editee = null;
  },

  /**
   * Attach this widget to a SVG element.
   *
   * @param aEditee The SVG element to attach.
   */
  attach: function attach(aEditee) {
    if (this._editee) {
      this.detach();
    }
    const C_i = Components.interfaces;

    if (!this._initialized) {
      var elementsList = [
        "rectClick",
        "gridMarker",
        "textOutput",
        "pointsSet",
        "enlargedItem",
        "gridControls",
        "gridControlsClear",
        "gridControlsMoveUp",
        "gridControlsMoveRight",
        "gridControlsMoveDown",
        "gridControlsMoveLeft",
        "gridControlsLine"
      ];

      var filter = {
        acceptNode: function acceptNode(aNode) {
          var className = aNode.getAttribute("class");
          if (!className)
            return C_i.nsIDOMNodeFilter.FILTER_SKIP;
          if (elementsList.indexOf(className) == -1)
            return C_i.nsIDOMNodeFilter.FILTER_SKIP;
          return C_i.nsIDOMNodeFilter.FILTER_ACCEPT;
        }
      }
      var walker = document.createTreeWalker(this._gridElement,
                                             C_i.nsIDOMNodeFilter.SHOW_ELEMENT,
                                             filter,
                                             true);
      this._elementMap = {};
      var elem;
      while ((elem = walker.nextNode())) {
        var className = elem.getAttribute("class");
        this._elementMap[className] = elem;
      }

      window.addEventListener("keydown", this._keyHandler, true);

      this._addCommandHandler("clearSelection",
                              this._elementMap.gridControlsClear,
                              "click");
      this._addKeyCommandHandler("clearSelection",
                                 C_i.nsIDOMKeyEvent.DOM_VK_ESCAPE);

      this._addCommandHandler("acceptSelection",
                              this._elementMap.rectClick,
                              "click");
      this._addKeyCommandHandler("acceptSelection",
                                 C_i.nsIDOMKeyEvent.DOM_VK_ENTER);
      this._addKeyCommandHandler("acceptSelection",
                                 C_i.nsIDOMKeyEvent.DOM_VK_RETURN);

      this._addCommandHandler("moveUp",
                              this._elementMap.gridControlsMoveUp,
                              "click");
      this._addKeyCommandHandler("moveUp",
                                 C_i.nsIDOMKeyEvent.DOM_VK_UP);

      this._addCommandHandler("moveRight",
                              this._elementMap.gridControlsMoveRight,
                              "click");
      this._addKeyCommandHandler("moveRight",
                                 C_i.nsIDOMKeyEvent.DOM_VK_RIGHT);

      this._addCommandHandler("moveDown",
                              this._elementMap.gridControlsMoveDown,
                              "click");
      this._addKeyCommandHandler("moveDown",
                                 C_i.nsIDOMKeyEvent.DOM_VK_DOWN);

      this._addCommandHandler("moveLeft",
                              this._elementMap.gridControlsMoveLeft,
                              "click");
      this._addKeyCommandHandler("moveLeft",
                                 C_i.nsIDOMKeyEvent.DOM_VK_LEFT);

      this._addCommandHandler("moveMouse",
                              this._elementMap.rectClick,
                              "mousemove");

      this._addCommandHandler("rectClickMouseOut",
                              this._elementMap.rectClick,
                              "mouseout");

      this._addCommandHandler("toggleLineDrawMode",
                              this._elementMap.gridControlsLine,
                              "click");
      this._addKeyCommandHandler("toggleLineDrawMode",
                                 C_i.nsIDOMKeyEvent.DOM_VK_L);

      this._mode = "select";
    }

    with (this._elementMap) {
      var range = document.createRange();
      range.selectNodeContents(aEditee);
      var frag = range.cloneContents();
      range.detach();
      range = null;
      delete range;

      enlargedItem.appendChild(frag);
    }

    this._editee = aEditee;
  }
};

/**
 * The base class for all SnapObjects (lines, circles, paths, etc.)
 */
function SnapObjectClass(aNewProperties) {
  for (var prop in aNewProperties) {
    this[prop] = aNewProperties[prop];
  }
}
SnapObjectClass.prototype = {
/******************************************************************************
 * Private methods for SnapObjectClass.                                       *
 ******************************************************************************/

  /**
   * Set the opacity of the element representing this point.
   *
   * @param aOpacity The opacity to set.
   */
  _setOpacity: function setOpacity(aOpacity) {
    this.checkInitialized();
    if (isNaN(aOpacity) ||
        (aOpacity < 0) ||
        (aOpacity > 1)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    this._element.style.opacity = aOpacity;
  },

/******************************************************************************
 * Public methods for SnapObjectClass.                                        *
 ******************************************************************************/

  /**
   * Initialize from a DOM Element.
   *
   * @param aElement The DOM element to initialize from.
   */
  initializeFromElement: function initializeFromElement(aElement) {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Initialize from a set of arguments.
   */
  initializeFromArguments: function initializeFromArguments() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Initialize from a set of SnapPoint objects.
   */
  initializeFromPoints: function initializeFromPoints() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Initialize by steps called from the user interface.
   *
   * @param aX The initial X coordinate to use for the next step.
   * @param aY The initial Y coordinate to use for the next step.
   *
   * @return SnapPoint object to use for the next step, or null if done.
   */
  initializeBySteps: function initializeBySteps(aX, aY) {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Ensure this object has not been detached.
   */
  checkNotDetached: function checkNotDetached() {
    if (this._detached) {
      throw Components.results.NS_ERROR_NOT_AVAILABLE;
    }
  },

  /**
   * Ensure this object has been initialized.
   */
  checkInitialized: function checkInitialized() {
    this.checkNotDetached();
    if (!this._initialized) {
      throw Components.results.NS_ERROR_NOT_INITIALIZED;
    }
  },

  /**
   * Ensure this object has not been initialized.
   */
  checkNotInitialized: function checkInitialized() {
    this.checkNotDetached();
    if (this._initialized) {
      throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
    }
  },

  /**
   * Move the object up by one pixel.
   */
  moveUp: function moveUp() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Move the object right bx one pixel.
   */
  moveRight: function moveRight() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Move the object down bx one pixel.
   */
  moveDown: function moveDown() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Move the object left by one pixel.
   */
  moveLeft: function moveLeft() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Move the object according to the mouse coordinates.
   */
  moveTo: function moveTo(aX, aY) {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  /**
   * Detach this object from the grid and destroy all references.
   */
  detach: function detach() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  }
};

/**
 * Build a object representing a point in the grid.
 */
function SnapPoint() {
  this.x = -1;
  this.y = -1;
  this._element = null;
  this._initialized = false;
  this._detached = false;
}
SnapPoint.prototype = new SnapObjectClass({
/******************************************************************************
 * Public properties for SnapPoint.                                           *
 ******************************************************************************/
  // SnapObjectClass
  initializeFromElement: function initializeFromElement(aElement) {
    this.checkNotInitialized();
    if (!(aElement instanceof Components.interfaces.nsIDOMSVGUseElement) ||
        (aElement.getAttribute("class") != "gridMarker")) {
      throw Components.results.NS_ERROR_ILLEGAL_VALUE;
    }

    var x = Number(aElement.getAttribute("x"));
    var y = Number(aElement.getAttribute("y"));

    if (isNaN(x)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }
    if (isNaN(y)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    this.x = x;
    this.y = y;
    this._element = aElement;
    this._initialized = true;
  },

  /**
   * Initialize the point by arguments.
   *
   * @param aX The X coordinate of the point.
   * @param aY The Y coordinate of the point.
   */
  initializeFromArguments: function initializeFromArguments(aX, aY) {
    this.checkNotInitialized();
    if (isNaN(aX)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }
    if (isNaN(aY)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    this.x = Number(aX);
    this.y = Number(aY);

    var gridMarker = SnapGrid._elementMap.gridMarker.cloneNode(true);
    gridMarker.setAttribute("x", aX);
    gridMarker.setAttribute("y", aY);
    this._element = gridMarker;

    this._initialized = true;
  },

  /**
   * Hide the SnapPoint from view.
   */
  hide: function hide() {
    this._setOpacity(0);
  },

  /**
   * Show the SnapPoint.
   */
  show: function show() {
    this._setOpacity(0.7);
  },

  /**
   * Show the SnapPoint as selected.
   */
  select: function select() {
    this._setOpacity(1);
  },

  /**
   * Determine if this point is equal to another SnapPoint.
   *
   * @param aSnapPoint The other point to compare to.
   */
  isEqual: function isEqual(aSnapPoint) {
    this.checkInitialized();
    if (!(aSnapPoint instanceof SnapPoint)) {
      throw Components.results.NS_ERROR_ILLEGAL_VALUE;
    }
    aSnapPoint.checkInitialized();
    return (this.x == aSnapPoint.x) && (this.y == aSnapPoint.y);
  },

  // SnapObjectClass
  moveUp: function moveUp() {
    this.checkInitialized();
    var y = this.y - 1;
    if (y < 0) {
      return;
    }

    this._element.setAttribute("y", y);
    this.y = y;
    this.select();
  },

  // SnapObjectClass
  moveRight: function moveRight() {
    this.checkInitialized();
    var x = this.x + 1;
    if (x < 0) {
      return;
    }

    this._element.setAttribute("x", x);
    this.x = x;
    this.select();
  },

  // SnapObjectClass
  moveDown: function moveDown() {
    this.checkInitialized();
    var y = this.y + 1;
    if (y < 0) {
      return;
    }

    this._element.setAttribute("y", y);
    this.y = y;
    this.select();
  },

  // SnapObjectClass
  moveLeft: function moveLeft() {
    this.checkInitialized();
    var x = this.x - 1;
    if (x < 0) {
      return;
    }

    this._element.setAttribute("x", x);
    this.x = x;
    this.select();
  },

  /**
   * Move the object according to the mouse coordinates.
   */
  moveTo: function moveTo(aX, aY) {
    this.checkInitialized();
    if ((aX < 0) || (aX > 30)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    if ((aY < 0) || (aY > 30)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    this._element.setAttribute("x", aX);
    this._element.setAttribute("y", aY);
    this.x = aX;
    this.y = aY;
    this.select();
  },

  // SnapObjectClass
  detach: function detach() {
    this._detached = true;

    if (this._element && this._element.parentNode)
      this._element.parentNode.removeChild(this._element);
    this._element = null;
  }
});

/**
 * Build an object representing a line in the grid.
 */
function SnapLine() {
  this._rendering = document.createElementNS(SVG_NS, "svg:g");
  this._points = [];
  this._element = null;
  this._initialized = false;
  this._detached = false;
}
SnapLine.prototype = new SnapObjectClass({
/******************************************************************************
 * Private properties for SnapLine.                                           *
 ******************************************************************************/

  /**
   * Finish initializing the object.
   */
  _finishInitializing: function finalizeInitialize() {
    this.checkNotInitialized();
    if (this._points.length != 2) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    if (!this._rendering.parentNode) {
      SnapGrid._elementMap.pointsSet.appendChild(this._rendering);
    }

    var line = document.createElementNS(SVG_NS, "svg:line");
    line.setAttribute("x1", this._points[0].x);
    line.setAttribute("y1", this._points[0].y);
    line.setAttribute("x2", this._points[1].x);
    line.setAttribute("y2", this._points[1].y);
    line.setAttribute("stroke", "#000000");

    this._element = line;
    SnapGrid._elementMap.enlargedItem.appendChild(line);

    this._initialized = true;
  },

/******************************************************************************
 * Public properties for SnapLine.                                            *
 ******************************************************************************/

  // SnapObjectClass
  initializeFromElement: function initializeFromElement(aElement) {
    this.checkNotInitialized();
    if (!(aElement instanceof Components.interfaces.nsIDOMSVGLineElement) ||
        (aElement.getAttribute("class") != "gridMarker")) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    var pt1 = new SnapPoint();
    pt1.initializeFromArguments(aElement.getAttribute("x1"),
                                aElement.getAttribute("y1"));
    pt1.show();
    this._rendering.appendChild(pt1._element);
    this._points.push(pt1);

    var pt2 = new SnapPoint();
    pt2.initializeFromArguments(aElement.getAttribute("x2"),
                                aElement.getAttribute("y2"));
    pt2.show();
    this._rendering.appendChild(pt2._element);
    this._points.push(pt2);

    this._finishInitializing();
  },

  /**
   * Initialize the line from a set of coordinates.
   *
   * @param aX1 The X coordinate of one end-point of the line.
   * @param aY1 The Y coordinate of one end-point of the line.
   * @param aX2 The X coordinate of the other end-point of the line.
   * @param aY2 The Y coordinate of the other end-point of the line.
   */
  initializeFromArguments: function initializeFromArguments() {
    this.checkNotInitialized();

    var pt1 = new SnapPoint();
    pt1.initializeFromArguments(arguments[0],
                                arguments[1]);
    pt1.show();
    this._rendering.appendChild(pt1._element);
    this._points.push(pt1);

    var pt2 = new SnapPoint();
    pt2.initializeFromArguments(arguments[2],
                                arguments[3]);
    pt2.show();
    this._rendering.appendChild(pt2._element);
    this._points.push(pt2);

    this._finishInitializing();
  },

  /**
   * Initialize from a set of SnapPoint objects.
   *
   * @param aPoint1 The first of two SnapPoint objects, representing an end-point.
   * @param aPoint2 The second of two SnapPoint objects, representing the other end-point.
   */
  initializeFromPoints: function initializeFromPoints() {
    this.checkNotInitialized();
    if (arguments.length != 2) {
      throw new Error("SnapPoint.initializeFromPoints takes exactly 2 SnapPoint arguments!");
    }

    if (!(arguments[0] instanceof SnapPoint) ||
        !(arguments[1] instanceof SnapPoint)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    this._rendering.appendChild(arguments[0]._element);
    this._points.push(arguments[0]);
    this._rendering.appendChild(arguments[1]._element);
    this._points.push(arguments[1]);
    this._finishInitializing();
  },

  // SnapObjectClass
  initializeBySteps: function initializeBySteps(aX, aY) {
    this.checkNotInitialized();

    if (typeof this._stepPoints == "undefined") {
      this._stepPoints = [];
      SnapGrid._elementMap.pointsSet.appendChild(this._rendering);
    } else {
      this._stepPoints[this._stepPoints.length - 1].show();
    }

    if (this._stepPoints.length < 2) {
      var point = new SnapPoint();
      point.initializeFromArguments(aX, aY);
      this._stepPoints.push(point);
      return point;
    }

    this.initializeFromPoints(this._stepPoints[0],
                              this._stepPoints[1]);
    this._stepPoints = undefined;
    delete this._stepPoints;
    return null;
  },

  // SnapObjectClass
  moveUp: function moveUp() {
    this.checkInitialized();
    var y = this._points[0].y - 1;
    if (y < 0) {
      return;
    }

    y = this._points[1].y - 1;
    if (y < 0) {
      return;
    }

    this._points[0].moveUp();
    this._points[1].moveUp();
    line.setAttribute("y1", this._points[0].y);
    line.setAttribute("y2", this._points[1].y);
  },

  // SnapObjectClass
  moveRight: function moveRight() {
    this.checkInitialized();
    var x = this._points[0].x + 1;
    if (x < 0) {
      return;
    }

    x = this._points[1].x + 1;
    if (x < 0) {
      return;
    }

    this._points[0].moveLeft();
    this._points[1].moveLeft();
    line.setAttribute("x1", this._points[0].x);
    line.setAttribute("x2", this._points[1].x);
  },

  // SnapObjectClass
  moveDown: function moveDown() {
    this.checkInitialized();
    var y = this._points[0].y + 1;
    if (y < 0) {
      return;
    }

    y = this._points[1].y + 1;
    if (y < 0) {
      return;
    }

    this._points[0].moveUp();
    this._points[1].moveUp();
    line.setAttribute("y1", this._points[0].y);
    line.setAttribute("y2", this._points[1].y);
  },

  // SnapObjectClass
  moveLeft: function moveLeft() {
    this.checkInitialized();
    var x = this._points[0].x - 1;
    if (x < 0) {
      return;
    }

    x = this._points[1].x - 1;
    if (x < 0) {
      return;
    }

    this._points[0].moveLeft();
    this._points[1].moveLeft();
    line.setAttribute("x1", this._points[0].x);
    line.setAttribute("x2", this._points[1].x);
  },


  // SnapObjectClass
  detach: function detach() {
    // This can be detached while not yet fully initialized.
    this._detached = true;

    var point;
    if (this._stepPoints) {
      while (this._stepPoints.length) {
        point = this._stepPoints[this._stepPoints.length - 1];
        if (point instanceof SnapPoint)
          point.detach();
        else
          dump("SnapLine.prototype.detach():  Not a SnapPoint?  " + point + "\n");
        this._stepPoints.length--;
      }
      this._stepPoints = undefined;
      delete this._stepPoints;
    }

    if (this._rendering.parentNode) {
      this._rendering.parentNode.removeChild(this._rendering);
    }
    this._rendering = null;

    while (this._points.length) {
      point = this._points[this._points.length - 1];
      if (point instanceof SnapPoint)
        point.detach();
      else
        dump("SnapLine.prototype.detach():  Not a SnapPoint?  " + point + "\n");
      this._points.length--;
    }

    // We do not remove this._element from its parent, in case the element remains.
    this._element = null;
  }
});
