<commit_before>if(!window.console) {
  window.console = new function() {
    this.log = function(str) {};
    this.dir = function(str) {};
  };
}

// These command objects are used for the Undo/Redo stack
// attrs contains the values that the attributes had before the change
function ChangeElementCommand(elem, attrs, text) {
	this.elem = elem;
	this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName);
	this.newValues = {};
	this.oldValues = attrs;
	for (attr in attrs) {
		if (attr == "#text") this.newValues[attr] = elem.textContent;
		else this.newValues[attr] = elem.getAttribute(attr);
	}

	this.apply = function() {
		for( attr in this.newValues ) {
			if (this.newValues[attr]) {
				if (attr == "#text") this.elem.textContent = this.newValues[attr];
				else this.elem.setAttribute(attr, this.newValues[attr]);
			}
			else {
				if (attr != "#text") this.elem.textContent = "";
				else this.elem.removeAttribute(attr);
			}
		}
		return true;
	};

	this.unapply = function() {
		for( attr in this.oldValues ) {
			if (this.oldValues[attr]) {
				if (attr == "#text") this.elem.textContent = this.oldValues[attr];
				else this.elem.setAttribute(attr, this.oldValues[attr]);
			}
			else {
				if (attr == "#text") this.elem.textContent = "";
				else this.elem.removeAttribute(attr);
			}
		}
		return true;
	};
}

function InsertElementCommand(elem, text) {
	this.elem = elem;
	this.text = text || ("Create " + elem.tagName);
	this.parent = elem.parentNode;

	this.apply = function() { this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); };

	this.unapply = function() {
		this.parent = this.elem.parentNode;
		this.elem = this.elem.parentNode.removeChild(this.elem);
	};
}

function RemoveElementCommand(elem, parent, text) {
	this.elem = elem;
	this.text = text || ("Delete " + elem.tagName);
	this.parent = parent;

	this.apply = function() {
		this.parent = this.elem.parentNode;
		this.elem = this.parent.removeChild(this.elem);
	};

	this.unapply = function() { this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); };
}

function MoveElementCommand(elem, oldNextSibling, oldParent, text) {
	this.elem = elem;
	this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName + "top/bottom");
	this.oldNextSibling = oldNextSibling;
	this.oldParent = oldParent;
	this.newNextSibling = elem.nextSibling;
	this.newParent = elem.parentNode;

	this.apply = function() {
		this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);
	};

	this.unapply = function() {
		this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);
	};
}

// TODO: create a 'typing' command object that tracks changes in text
// if a new Typing command is created and the top command on the stack is also a Typing
// and they both affect the same element, then collapse the two commands into one

// TODO: create a 'batch' command object that you can add an arbitrary number of 
// commands to (this will be useful for making changes to many elements in a 
// multiselection at one, we don't want to have to undo each element's change)

function SvgCanvas(c)
{
// private members

	// **************************************************************************************
	// FIXME: what's the right way to make this 'class' private to the SelectorManager? 
	function Selector(id, elem) {
		// this is the selector's unique number
		this.id = id;
	
		// this holds a reference to the element for which this selector is being used
		this.selectedElement = elem;
	
		// this is a flag used internally to track whether the selector is being used or not
		this.locked = true;
	
		// this function is used to reset the id and element that the selector is attached to
		this.reset = function(e) {
			this.locked = true;
			this.selectedElement = e;
			this.selectorGroup.setAttribute("display", "inline");
		};
	
		// this holds a reference to the <g> element that holds all visual elements of the selector
		this.selectorGroup = addSvgElementFromJson({ "element": "g",
													"attr": {"id": ("selectorGroup"+this.id)}
													});
	
		// this holds a reference to <rect> element
		this.selectorRect = this.selectorGroup.appendChild( addSvgElementFromJson({
								"element": "rect",
								"attr": {
									"id": ("selectedBox"+this.id),
									"fill": "none",
									"stroke": "blue",
									"stroke-width": "1",
									"stroke-dasharray": "5,5",
									"width": 1,
									"height": 1,
									// need to specify this so that the rect is not selectable
									"style": "pointer-events:none",
								}
							}) );
	
		// this holds a reference to the grip elements for this selector
		this.selectorGrips = {	"nw":null,
								"n":null,
								"ne":null,
								"w":null,
								"e":null,
								"sw":null,
								"s":null,
								"se":null,
								};
				
		// add the corner grips
		for (dir in this.selectorGrips) {
			this.selectorGrips[dir] = this.selectorGroup.appendChild( addSvgElementFromJson({
										"element": "rect",
										"attr": {
											"id": (dir + "_grip" + this.id),
											"fill": "blue",
											"width": 6,
											"height": 6,
											"style": ("cursor:" + dir + "-resize"),
											// when we are in rotate mode, we will set rx/ry to 3
//											"rx": 3,
//											"ry": 3,
											// This expands the mouse-able area of the grips making them
											// easier to grab with the mouse.
											// This works in Opera and WebKit, but does not work in Firefox
											// see https://bugzilla.mozilla.org/show_bug.cgi?id=500174
											"stroke-width": 2,
											"pointer-events": "all",
										}
									}) );
			$('#'+this.selectorGrips[dir].id).mousedown( function() {
				current_mode = "resize";
				current_resize_mode = this.id.substr(0,this.id.indexOf("_"));
			});
		}
	
		this.showGrips = function(show) {
			for (dir in this.selectorGrips) {
				this.selectorGrips[dir].setAttribute("display", show ? "inline" : "none");
			}
		};
		
		this.resize = function(bbox) {
			var selectedBox = this.selectorRect;
			var selectedGrips = this.selectorGrips;
			var selected = this.selectedElement;
			var sw = parseInt(selected.getAttribute("stroke-width"));
			var offset = 1;
			if (!isNaN(sw)) {
				offset += sw/2;
			}
			if (selected.tagName == "text") {
				offset += 2;
			}
			var l=bbox.x-offset, t=bbox.y-offset, w=bbox.width+(offset<<1), h=bbox.height+(offset<<1);
			selectedBox.x.baseVal.value = l;
			selectedBox.y.baseVal.value = t;
			selectedBox.width.baseVal.value = w;
			selectedBox.height.baseVal.value = h;
			selectedGrips.nw.x.baseVal.value = l-3;
			selectedGrips.nw.y.baseVal.value = t-3;
			selectedGrips.ne.x.baseVal.value = l+w-3;
			selectedGrips.ne.y.baseVal.value = t-3;
			selectedGrips.sw.x.baseVal.value = l-3;
			selectedGrips.sw.y.baseVal.value = t+h-3;
			selectedGrips.se.x.baseVal.value = l+w-3;
			selectedGrips.se.y.baseVal.value = t+h-3;
			selectedGrips.n.x.baseVal.value = l+w/2-3;
			selectedGrips.n.y.baseVal.value = t-3;
			selectedGrips.w.x.baseVal.value = l-3;
			selectedGrips.w.y.baseVal.value = t+h/2-3;
			selectedGrips.e.x.baseVal.value = l+w-3;
			selectedGrips.e.y.baseVal.value = t+h/2-3;
			selectedGrips.s.x.baseVal.value = l+w/2-3;
			selectedGrips.s.y.baseVal.value = t+h-3;
		};
	};

	// TODO: consider a map of SVG elements to their selectors in the manager for
	// quick access to the selector in question (as opposed to looping through the array)
	function SelectorManager() {
		
		// this will hold the <g> element that contains all selector rects/grips
		this.selectorParentGroup = null;
	
		// this is a special rect that is used for multi-select
		this.rubberBandBox = null;
	
		// this will hold objects of type Selector (see above)
		this.selectors = [];

		// local reference to this object
		var mgr = this;
		// private function
		var initGroup = function() {
			mgr.selectorParentGroup = addSvgElementFromJson({
											"element": "g",
											"attr": {"id": "selectorParentGroup"}
										});			
		};
					
		this.requestSelector = function(elem) {
			var N = this.selectors.length;

			if (this.selectorParentGroup == null) {
				initGroup();											
			}

			for (var i = 0; i < N; ++i) {
				if (this.selectors[i] && !this.selectors[i].locked) {
					this.selectors[i].locked = true;
					this.selectors[i].reset(elem);					
					return this.selectors[i];
				}
			}
			// if we reached here, no available selectors were found, we create one
			this.selectors[N] = new Selector(N, elem);
			this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);
			return this.selectors[N];
		};
		this.releaseSelector = function(sel) {
			var N = this.selectors.length;
			for (var i = 0; i < N; ++i) {
				if (this.selectors[i] && this.selectors[i] == sel) {
					if (sel.locked == false) {
						console.log("WARNING! selector was released but was already unlocked");
					}
					sel.locked = false;
					sel.selectedElement = null;

					// remove from DOM and store reference in JS but only if it exists in the DOM
					try {
						sel.selectorGroup.setAttribute("display", "none");
					} catch(e) { }
					
					break;
				}
			}
		};
		
		// this keeps the selector groups as the last child in the document
		this.update = function() {
			this.selectorParentGroup = svgroot.appendChild(this.selectorParentGroup);
		}
		
		this.getRubberBandBox = function() {
			if (this.selectorParentGroup == null) {
				initGroup();
			}
			
			if (this.rubberBandBox == null) {
				this.rubberBandBox = addSvgElementFromJson({ "element": "rect",
															"attr": {
																"id": "selectorRubberBand",
																"fill": "blue",
																"fill-opacity": 0.15,
																"display": "none",
																"pointer-events": "none",
																}
															});
			}
			return this.rubberBandBox;
		}
	}
	// **************************************************************************************

	var canvas = this;
	var container = c;
	var svgns = "http://www.w3.org/2000/svg";

	var idprefix = "svg_";
	var svgdoc  = c.ownerDocument;
	var svgroot = svgdoc.createElementNS(svgns, "svg");
	svgroot.setAttribute("width", 640);
	svgroot.setAttribute("height", 480);
	svgroot.setAttribute("id", "svgroot");
	svgroot.setAttribute("xmlns", svgns);
	container.appendChild(svgroot);

	var d_attr = null;
	var started = false;
	var obj_num = 1;
	var start_x = null;
	var start_y = null;
	var current_mode = "select";
	var current_resize_mode = "none";
	var current_fill = "none";
	var current_stroke = "black";
	var current_stroke_width = 1;
	var current_stroke_style = "none";
	var current_opacity = 1;
	var current_stroke_opacity = 1;
	var current_fill_opacity = 1;
	var current_font_size = "12pt";
	var current_font_family = "serif";
	var freehand_min_x = null;
	var freehand_max_x = null;
	var freehand_min_y = null;
	var freehand_max_y = null;
	// this will hold all the currently selected elements
	// default size of 1 until it needs to grow bigger
	var selectedElements = new Array(1); 
	// this holds the selected's bbox
	var selectedBBox = null;
	// this object manages selectors for us
	var selectorManager = new SelectorManager();
	// this object holds the one-and-only selector (for now)
	var theSelector = null;
	var rubberBox = null;
	var events = {};
	var undoStackPointer = 0;
	var undoStack = [];

	// FIXME: we MUST compress consecutive text changes to the same element
	// (right now each keystroke is saved as a separate command that includes the
	// entire text contents of the text element)
	// TODO: consider limiting the history that we store here (need to do some slicing)
	var addCommandToHistory = function(cmd) {
		// if our stack pointer is not at the end, then we have to remove
		// all commands after the pointer and insert the new command
		if (undoStackPointer < undoStack.length && undoStack.length > 0) {
			undoStack = undoStack.splice(0, undoStackPointer);
		}
		undoStack[undoStack.length] = cmd;
		undoStackPointer = undoStack.length;
//		console.log("after add command, stackPointer=" + undoStackPointer);
//		console.log(undoStack);
	};

// private functions
	var getId = function() {
		if (events["getid"]) return call("getid", obj_num);
		return idprefix + obj_num;
	};

	var call = function(event, arg) {
		if (events[event]) {
			return events[event](this,arg);
		}
	};

	var assignAttributes = function(node, attrs) {
		for (i in attrs) {
			node.setAttributeNS(null, i, attrs[i]);
		}
	};

	// remove unneeded attributes
	// makes resulting SVG smaller
	var cleanupElement = function(element) {
		if (element.getAttribute('fill-opacity') == '1')
			element.removeAttribute('fill-opacity');
		if (element.getAttribute('opacity') == '1')
			element.removeAttribute('opacity');
		if (element.getAttribute('stroke') == 'none')
			element.removeAttribute('stroke');
		if (element.getAttribute('stroke-dasharray') == 'none')
			element.removeAttribute('stroke-dasharray');
		if (element.getAttribute('stroke-opacity') == '1')
			element.removeAttribute('stroke-opacity');
		if (element.getAttribute('stroke-width') == '1')
			element.removeAttribute('stroke-width');
		if (element.getAttribute('rx') == '0')
			element.removeAttribute('rx')
		if (element.getAttribute('ry') == '0')
			element.removeAttribute('ry')
	};

	var addSvgElementFromJson = function(data) {
		return canvas.updateElementFromJson(data)
	};

	var svgToString = function(elem, indent) {
		// TODO: could use the array.join() optimization trick here too
		var out = "";
		if (elem) {
			var attrs = elem.attributes;
			var attr;
			var i;
			var childs = elem.childNodes;
			for (i=0; i<indent; i++) out += " ";
			out += "<" + elem.nodeName;
			for (i=attrs.length-1; i>=0; i--) {
				attr = attrs.item(i);
				if (attr.nodeValue != "") {
					out += " " + attr.nodeName + "=\"" + attr.nodeValue+ "\"";
				}
			}
			if (elem.hasChildNodes()) {
				out += ">";
				indent++;
				var bOneLine = false;
				for (i=0; i<childs.length; i++)
				{
					if (childs.item(i).nodeType == 1) { // element node
						out += "\n" + svgToString(childs.item(i), indent);
					} else if (childs.item(i).nodeType == 3) { // text node
						bOneLine = true;
						out += childs.item(i).nodeValue + "";
					}
				}
				indent--;
				if (!bOneLine) {
					out += "\n";
					for (i=0; i<indent; i++) out += " ";
				}
				out += "</" + elem.nodeName + ">";
			} else {
				out += "/>";
			}
		}
		return out;
	}; // end svgToString()

	var recalculateSelectedDimensions = function() {
		var selected = selectedElements[0];
		var box = selected.getBBox();

		// if we have not moved/resized, then immediately leave
		if (box.x == selectedBBox.x && box.y == selectedBBox.y &&
			box.width == selectedBBox.width && box.height == selectedBBox.height) {
			return;
		}

		// after this point, we have some change

		var remapx = function(x) {return ((x-box.x)/box.width)*selectedBBox.width + selectedBBox.x;}
		var remapy = function(y) {return ((y-box.y)/box.height)*selectedBBox.height + selectedBBox.y;}
		var scalew = function(w) {return w*selectedBBox.width/box.width;}
		var scaleh = function(h) {return h*selectedBBox.height/box.height;}

		var changes = {};

		selected.removeAttribute("transform");
		switch (selected.tagName)
		{
			case "path":
				// extract the x,y from the path, adjust it and write back the new path
				// but first, save the old path
				changes["d"] = selected.getAttribute("d");
				var M = selected.pathSegList.getItem(0);
				var curx = M.x, cury = M.y;
				var newd = "M" + remapx(curx) + "," + remapy(cury);
				for (var i = 1; i < selected.pathSegList.numberOfItems; ++i) {
					var l = selected.pathSegList.getItem(i);
					var x = l.x, y = l.y;
					// webkit browsers normalize things and this becomes an absolute
					// line segment!  we need to turn this back into a rel line segment
					// see https://bugs.webkit.org/show_bug.cgi?id=26487
					if (l.pathSegType == 4) {
						x -= curx;
						y -= cury;
						curx += x;
						cury += y;
					}
					// we only need to scale the relative coordinates (no need to translate)
					newd += " l" + scalew(x) + "," + scaleh(y);
				}
				selected.setAttributeNS(null, "d", newd);
				break;
			case "line":
				changes["x1"] = selected.x1.baseVal.value;
				changes["y1"] = selected.y1.baseVal.value;
				changes["x2"] = selected.x2.baseVal.value;
				changes["y2"] = selected.y2.baseVal.value;
				selected.x1.baseVal.value = remapx(selected.x1.baseVal.value);
				selected.y1.baseVal.value = remapy(selected.y1.baseVal.value);
				selected.x2.baseVal.value = remapx(selected.x2.baseVal.value);
				selected.y2.baseVal.value = remapy(selected.y2.baseVal.value);
				break;
			case "circle":
				changes["cx"] = selected.cx.baseVal.value;
				changes["cy"] = selected.cy.baseVal.value;
				changes["r"] = selected.r.baseVal.value;
				selected.cx.baseVal.value = remapx(selected.cx.baseVal.value);
				selected.cy.baseVal.value = remapy(selected.cy.baseVal.value);
				// take the minimum of the new selected box's dimensions for the new circle radius
				selected.r.baseVal.value = Math.min(selectedBBox.width/2,selectedBBox.height/2);
				break;
			case "ellipse":
				changes["cx"] = selected.cx.baseVal.value;
				changes["cy"] = selected.cy.baseVal.value;
				changes["rx"] = selected.rx.baseVal.value;
				changes["ry"] = selected.ry.baseVal.value;
				selected.cx.baseVal.value = remapx(selected.cx.baseVal.value);
				selected.cy.baseVal.value = remapy(selected.cy.baseVal.value);
				selected.rx.baseVal.value = scalew(selected.rx.baseVal.value);
				selected.ry.baseVal.value = scaleh(selected.ry.baseVal.value);
				break;
			case "text":
				// cannot use x.baseVal.value here because x is a SVGLengthList
				changes["x"] = selected.getAttribute("x");
				changes["y"] = selected.getAttribute("y");
				selected.setAttribute("x", remapx(selected.getAttribute("x")));
				selected.setAttribute("y", remapy(selected.getAttribute("y")));
				break;
			case "rect":
				changes["x"] = selected.x.baseVal.value;
				changes["y"] = selected.y.baseVal.value;
				changes["width"] = selected.width.baseVal.value;
				changes["height"] = selected.height.baseVal.value;
				selected.x.baseVal.value = remapx(selected.x.baseVal.value);
				selected.y.baseVal.value = remapy(selected.y.baseVal.value);
				selected.width.baseVal.value = scalew(selected.width.baseVal.value);
				selected.height.baseVal.value = scaleh(selected.height.baseVal.value);
				break;
			default: // rect
				console.log("Unknown shape type: " + selected.tagName);
				break;
		}
		// fire changed event
		if (changes) {
			var text = (current_resize_mode == "none" ? "position" : "size");
			addCommandToHistory(new ChangeElementCommand(selected, changes, text));
		}
		call("changed", selected);
	};

	var recalculateSelectedOutline = function() {
		var selected = selectedElements[0];
		if (selected != null && theSelector != null) {
			theSelector.resize(selectedBBox);
		}
	};

// public events
	// call this function to set a single selected element
	// call this function with null to clear the selected element
	var selectElement = function(newSelected)
	{
		var selected = selectedElements[0];	
		if (selected == newSelected) return;

		// remove selected outline from previously selected element
		if (theSelector != null) {
			selectorManager.releaseSelector(theSelector);
		}

		selectedElements[0] = newSelected;
		selected = selectedElements[0];

		if (selected != null) {
			selectedBBox = selected.getBBox();

			// the manager gives us a selector
			theSelector = selectorManager.requestSelector(selected);
			
			// recalculate size and then re-append to bottom of document
			recalculateSelectedOutline();

			// set all our current styles to the selected styles
			current_fill = selected.getAttribute("fill");
			current_fill_opacity = selected.getAttribute("fill-opacity");
			current_stroke = selected.getAttribute("stroke");
			current_stroke_opacity = selected.getAttribute("stroke-opacity");
			current_stroke_width = selected.getAttribute("stroke-width");
			current_stroke_style = selected.getAttribute("stroke-dasharray");
			if (selected.tagName == "text") {
				current_font_size = selected.getAttribute("font-size");
				current_font_family = selected.getAttribute("font-family");
			}

			// do not show resize grips on text elements
			theSelector.showGrips(selected.tagName != "text");
		}

		call("selected", selected);
	};

	// in mouseDown :
	// - when we are in a create mode, the element is added to the canvas
	//   but the action is not recorded until mouseUp
	// - when we are in select mode, select the element, remember the position
	//   and do nothing else
	var mouseDown = function(evt)
	{<commit_msg>More minor tweaks, still no multi-select<commit_after>if(!window.console) {
  window.console = new function() {
    this.log = function(str) {};
    this.dir = function(str) {};
  };
}

// These command objects are used for the Undo/Redo stack
// attrs contains the values that the attributes had before the change
function ChangeElementCommand(elem, attrs, text) {
	this.elem = elem;
	this.text = text ? ("Change " + elem.tagName + " " + text) : ("Change " + elem.tagName);
	this.newValues = {};
	this.oldValues = attrs;
	for (attr in attrs) {
		if (attr == "#text") this.newValues[attr] = elem.textContent;
		else this.newValues[attr] = elem.getAttribute(attr);
	}

	this.apply = function() {
		for( attr in this.newValues ) {
			if (this.newValues[attr]) {
				if (attr == "#text") this.elem.textContent = this.newValues[attr];
				else this.elem.setAttribute(attr, this.newValues[attr]);
			}
			else {
				if (attr != "#text") this.elem.textContent = "";
				else this.elem.removeAttribute(attr);
			}
		}
		return true;
	};

	this.unapply = function() {
		for( attr in this.oldValues ) {
			if (this.oldValues[attr]) {
				if (attr == "#text") this.elem.textContent = this.oldValues[attr];
				else this.elem.setAttribute(attr, this.oldValues[attr]);
			}
			else {
				if (attr == "#text") this.elem.textContent = "";
				else this.elem.removeAttribute(attr);
			}
		}
		return true;
	};
}

function InsertElementCommand(elem, text) {
	this.elem = elem;
	this.text = text || ("Create " + elem.tagName);
	this.parent = elem.parentNode;

	this.apply = function() { this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); };

	this.unapply = function() {
		this.parent = this.elem.parentNode;
		this.elem = this.elem.parentNode.removeChild(this.elem);
	};
}

function RemoveElementCommand(elem, parent, text) {
	this.elem = elem;
	this.text = text || ("Delete " + elem.tagName);
	this.parent = parent;

	this.apply = function() {
		this.parent = this.elem.parentNode;
		this.elem = this.parent.removeChild(this.elem);
	};

	this.unapply = function() { this.elem = this.parent.insertBefore(this.elem, this.elem.nextSibling); };
}

function MoveElementCommand(elem, oldNextSibling, oldParent, text) {
	this.elem = elem;
	this.text = text ? ("Move " + elem.tagName + " to " + text) : ("Move " + elem.tagName + "top/bottom");
	this.oldNextSibling = oldNextSibling;
	this.oldParent = oldParent;
	this.newNextSibling = elem.nextSibling;
	this.newParent = elem.parentNode;

	this.apply = function() {
		this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling);
	};

	this.unapply = function() {
		this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling);
	};
}

// TODO: create a 'typing' command object that tracks changes in text
// if a new Typing command is created and the top command on the stack is also a Typing
// and they both affect the same element, then collapse the two commands into one

// TODO: create a 'batch' command object that you can add an arbitrary number of 
// commands to (this will be useful for making changes to many elements in a 
// multiselection at one, we don't want to have to undo each element's change)

function SvgCanvas(c)
{
// private members

	// **************************************************************************************
	// FIXME: what's the right way to make this 'class' private to the SelectorManager? 
	function Selector(id, elem) {
		// this is the selector's unique number
		this.id = id;
	
		// this holds a reference to the element for which this selector is being used
		this.selectedElement = elem;
	
		// this is a flag used internally to track whether the selector is being used or not
		this.locked = true;
	
		// this function is used to reset the id and element that the selector is attached to
		this.reset = function(e) {
			this.locked = true;
			this.selectedElement = e;
			this.selectorGroup.setAttribute("display", "inline");
		};
	
		// this holds a reference to the <g> element that holds all visual elements of the selector
		this.selectorGroup = addSvgElementFromJson({ "element": "g",
													"attr": {"id": ("selectorGroup"+this.id)}
													});
	
		// this holds a reference to <rect> element
		this.selectorRect = this.selectorGroup.appendChild( addSvgElementFromJson({
								"element": "rect",
								"attr": {
									"id": ("selectedBox"+this.id),
									"fill": "none",
									"stroke": "blue",
									"stroke-width": "1",
									"stroke-dasharray": "5,5",
									"width": 1,
									"height": 1,
									// need to specify this so that the rect is not selectable
									"style": "pointer-events:none",
								}
							}) );
	
		// this holds a reference to the grip elements for this selector
		this.selectorGrips = {	"nw":null,
								"n":null,
								"ne":null,
								"w":null,
								"e":null,
								"sw":null,
								"s":null,
								"se":null,
								};
				
		// add the corner grips
		for (dir in this.selectorGrips) {
			this.selectorGrips[dir] = this.selectorGroup.appendChild( addSvgElementFromJson({
										"element": "rect",
										"attr": {
											"id": (dir + "_grip" + this.id),
											"fill": "blue",
											"width": 6,
											"height": 6,
											"style": ("cursor:" + dir + "-resize"),
											// when we are in rotate mode, we will set rx/ry to 3
//											"rx": 3,
//											"ry": 3,
											// This expands the mouse-able area of the grips making them
											// easier to grab with the mouse.
											// This works in Opera and WebKit, but does not work in Firefox
											// see https://bugzilla.mozilla.org/show_bug.cgi?id=500174
											"stroke-width": 2,
											"pointer-events": "all",
										}
									}) );
			$('#'+this.selectorGrips[dir].id).mousedown( function() {
				current_mode = "resize";
				current_resize_mode = this.id.substr(0,this.id.indexOf("_"));
			});
		}
	
		this.showGrips = function(show) {
			for (dir in this.selectorGrips) {
				this.selectorGrips[dir].setAttribute("display", show ? "inline" : "none");
			}
		};
		
		this.resize = function(bbox) {
			var selectedBox = this.selectorRect;
			var selectedGrips = this.selectorGrips;
			var selected = this.selectedElement;
			var sw = parseInt(selected.getAttribute("stroke-width"));
			var offset = 1;
			if (!isNaN(sw)) {
				offset += sw/2;
			}
			if (selected.tagName == "text") {
				offset += 2;
			}
			var l=bbox.x-offset, t=bbox.y-offset, w=bbox.width+(offset<<1), h=bbox.height+(offset<<1);
			selectedBox.x.baseVal.value = l;
			selectedBox.y.baseVal.value = t;
			selectedBox.width.baseVal.value = w;
			selectedBox.height.baseVal.value = h;
			selectedGrips.nw.x.baseVal.value = l-3;
			selectedGrips.nw.y.baseVal.value = t-3;
			selectedGrips.ne.x.baseVal.value = l+w-3;
			selectedGrips.ne.y.baseVal.value = t-3;
			selectedGrips.sw.x.baseVal.value = l-3;
			selectedGrips.sw.y.baseVal.value = t+h-3;
			selectedGrips.se.x.baseVal.value = l+w-3;
			selectedGrips.se.y.baseVal.value = t+h-3;
			selectedGrips.n.x.baseVal.value = l+w/2-3;
			selectedGrips.n.y.baseVal.value = t-3;
			selectedGrips.w.x.baseVal.value = l-3;
			selectedGrips.w.y.baseVal.value = t+h/2-3;
			selectedGrips.e.x.baseVal.value = l+w-3;
			selectedGrips.e.y.baseVal.value = t+h/2-3;
			selectedGrips.s.x.baseVal.value = l+w/2-3;
			selectedGrips.s.y.baseVal.value = t+h-3;
		};
	};

	// TODO: add accessor methods to determine number of currently selected elements
	// TODO: move selectElement into SelectorManager and allow multiple selected elements
	//       with each call
	function SelectorManager() {
		
		// this will hold the <g> element that contains all selector rects/grips
		this.selectorParentGroup = null;
	
		// this is a special rect that is used for multi-select
		this.rubberBandBox = null;
	
		// this will hold objects of type Selector (see above)
		this.selectors = [];
		
		// this holds a map of SVG elements to their Selector object
		this.selectorMap = {};

		// local reference to this object
		var mgr = this;
		// private function
		var initGroup = function() {
			mgr.selectorParentGroup = addSvgElementFromJson({
											"element": "g",
											"attr": {"id": "selectorParentGroup"}
										});			
		};
					
		this.requestSelector = function(elem) {
			var N = this.selectors.length;

			if (this.selectorParentGroup == null) {
				initGroup();											
			}

			for (var i = 0; i < N; ++i) {
				if (this.selectors[i] && !this.selectors[i].locked) {
					this.selectors[i].locked = true;
					this.selectors[i].reset(elem);					
					return this.selectors[i];
				}
			}
			// if we reached here, no available selectors were found, we create one
			this.selectors[N] = new Selector(N, elem);
			this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);
			this.selectorMap[elem] = this.selectors[N];
			return this.selectors[N];
		};
		this.releaseSelector = function(sel) {
			var N = this.selectors.length;
			for (var i = 0; i < N; ++i) {
				if (this.selectors[i] && this.selectors[i] == sel) {
					if (sel.locked == false) {
						console.log("WARNING! selector was released but was already unlocked");
					}
					delete this.selectorMap[sel.selectedElement];
					sel.locked = false;
					sel.selectedElement = null;

					// remove from DOM and store reference in JS but only if it exists in the DOM
					try {
						sel.selectorGroup.setAttribute("display", "none");
					} catch(e) { }
					
					break;
				}
			}
		};
		
		// this keeps the selector groups as the last child in the document
		this.update = function() {
			this.selectorParentGroup = svgroot.appendChild(this.selectorParentGroup);
		}
		
		this.getRubberBandBox = function() {
			if (this.selectorParentGroup == null) {
				initGroup();
			}
			
			if (this.rubberBandBox == null) {
				this.rubberBandBox = addSvgElementFromJson({ "element": "rect",
															"attr": {
																"id": "selectorRubberBand",
																"fill": "blue",
																"fill-opacity": 0.15,
																"display": "none",
																"pointer-events": "none",
																}
															});
			}
			return this.rubberBandBox;
		}
	}
	// **************************************************************************************

	var canvas = this;
	var container = c;
	var svgns = "http://www.w3.org/2000/svg";

	var idprefix = "svg_";
	var svgdoc  = c.ownerDocument;
	var svgroot = svgdoc.createElementNS(svgns, "svg");
	svgroot.setAttribute("width", 640);
	svgroot.setAttribute("height", 480);
	svgroot.setAttribute("id", "svgroot");
	svgroot.setAttribute("xmlns", svgns);
	container.appendChild(svgroot);

	var d_attr = null;
	var started = false;
	var obj_num = 1;
	var start_x = null;
	var start_y = null;
	var current_mode = "select";
	var current_resize_mode = "none";
	var current_fill = "none";
	var current_stroke = "black";
	var current_stroke_width = 1;
	var current_stroke_style = "none";
	var current_opacity = 1;
	var current_stroke_opacity = 1;
	var current_fill_opacity = 1;
	var current_font_size = "12pt";
	var current_font_family = "serif";
	var freehand_min_x = null;
	var freehand_max_x = null;
	var freehand_min_y = null;
	var freehand_max_y = null;
	// this will hold all the currently selected elements
	// default size of 1 until it needs to grow bigger
	var selectedElements = new Array(1); 
	// this holds the selected's bbox
	var selectedBBox = null;
	// this object manages selectors for us
	var selectorManager = new SelectorManager();
	// this object holds the one-and-only selector (for now)
	var theSelector = null;
	var rubberBox = null;
	var events = {};
	var undoStackPointer = 0;
	var undoStack = [];

	// FIXME: we MUST compress consecutive text changes to the same element
	// (right now each keystroke is saved as a separate command that includes the
	// entire text contents of the text element)
	// TODO: consider limiting the history that we store here (need to do some slicing)
	var addCommandToHistory = function(cmd) {
		// if our stack pointer is not at the end, then we have to remove
		// all commands after the pointer and insert the new command
		if (undoStackPointer < undoStack.length && undoStack.length > 0) {
			undoStack = undoStack.splice(0, undoStackPointer);
		}
		undoStack[undoStack.length] = cmd;
		undoStackPointer = undoStack.length;
//		console.log("after add command, stackPointer=" + undoStackPointer);
//		console.log(undoStack);
	};

// private functions
	var getId = function() {
		if (events["getid"]) return call("getid", obj_num);
		return idprefix + obj_num;
	};

	var call = function(event, arg) {
		if (events[event]) {
			return events[event](this,arg);
		}
	};

	var assignAttributes = function(node, attrs) {
		for (i in attrs) {
			node.setAttributeNS(null, i, attrs[i]);
		}
	};

	// remove unneeded attributes
	// makes resulting SVG smaller
	var cleanupElement = function(element) {
		if (element.getAttribute('fill-opacity') == '1')
			element.removeAttribute('fill-opacity');
		if (element.getAttribute('opacity') == '1')
			element.removeAttribute('opacity');
		if (element.getAttribute('stroke') == 'none')
			element.removeAttribute('stroke');
		if (element.getAttribute('stroke-dasharray') == 'none')
			element.removeAttribute('stroke-dasharray');
		if (element.getAttribute('stroke-opacity') == '1')
			element.removeAttribute('stroke-opacity');
		if (element.getAttribute('stroke-width') == '1')
			element.removeAttribute('stroke-width');
		if (element.getAttribute('rx') == '0')
			element.removeAttribute('rx')
		if (element.getAttribute('ry') == '0')
			element.removeAttribute('ry')
	};

	var addSvgElementFromJson = function(data) {
		return canvas.updateElementFromJson(data)
	};

	var svgToString = function(elem, indent) {
		// TODO: could use the array.join() optimization trick here too
		var out = "";
		if (elem) {
			var attrs = elem.attributes;
			var attr;
			var i;
			var childs = elem.childNodes;
			for (i=0; i<indent; i++) out += " ";
			out += "<" + elem.nodeName;
			for (i=attrs.length-1; i>=0; i--) {
				attr = attrs.item(i);
				if (attr.nodeValue != "") {
					out += " " + attr.nodeName + "=\"" + attr.nodeValue+ "\"";
				}
			}
			if (elem.hasChildNodes()) {
				out += ">";
				indent++;
				var bOneLine = false;
				for (i=0; i<childs.length; i++)
				{
					if (childs.item(i).nodeType == 1) { // element node
						out += "\n" + svgToString(childs.item(i), indent);
					} else if (childs.item(i).nodeType == 3) { // text node
						bOneLine = true;
						out += childs.item(i).nodeValue + "";
					}
				}
				indent--;
				if (!bOneLine) {
					out += "\n";
					for (i=0; i<indent; i++) out += " ";
				}
				out += "</" + elem.nodeName + ">";
			} else {
				out += "/>";
			}
		}
		return out;
	}; // end svgToString()

	var recalculateSelectedDimensions = function() {
		var selected = selectedElements[0];
		var box = selected.getBBox();

		// if we have not moved/resized, then immediately leave
		if (box.x == selectedBBox.x && box.y == selectedBBox.y &&
			box.width == selectedBBox.width && box.height == selectedBBox.height) {
			return;
		}

		// after this point, we have some change

		var remapx = function(x) {return ((x-box.x)/box.width)*selectedBBox.width + selectedBBox.x;}
		var remapy = function(y) {return ((y-box.y)/box.height)*selectedBBox.height + selectedBBox.y;}
		var scalew = function(w) {return w*selectedBBox.width/box.width;}
		var scaleh = function(h) {return h*selectedBBox.height/box.height;}

		var changes = {};

		selected.removeAttribute("transform");
		switch (selected.tagName)
		{
			case "path":
				// extract the x,y from the path, adjust it and write back the new path
				// but first, save the old path
				changes["d"] = selected.getAttribute("d");
				var M = selected.pathSegList.getItem(0);
				var curx = M.x, cury = M.y;
				var newd = "M" + remapx(curx) + "," + remapy(cury);
				for (var i = 1; i < selected.pathSegList.numberOfItems; ++i) {
					var l = selected.pathSegList.getItem(i);
					var x = l.x, y = l.y;
					// webkit browsers normalize things and this becomes an absolute
					// line segment!  we need to turn this back into a rel line segment
					// see https://bugs.webkit.org/show_bug.cgi?id=26487
					if (l.pathSegType == 4) {
						x -= curx;
						y -= cury;
						curx += x;
						cury += y;
					}
					// we only need to scale the relative coordinates (no need to translate)
					newd += " l" + scalew(x) + "," + scaleh(y);
				}
				selected.setAttributeNS(null, "d", newd);
				break;
			case "line":
				changes["x1"] = selected.x1.baseVal.value;
				changes["y1"] = selected.y1.baseVal.value;
				changes["x2"] = selected.x2.baseVal.value;
				changes["y2"] = selected.y2.baseVal.value;
				selected.x1.baseVal.value = remapx(selected.x1.baseVal.value);
				selected.y1.baseVal.value = remapy(selected.y1.baseVal.value);
				selected.x2.baseVal.value = remapx(selected.x2.baseVal.value);
				selected.y2.baseVal.value = remapy(selected.y2.baseVal.value);
				break;
			case "circle":
				changes["cx"] = selected.cx.baseVal.value;
				changes["cy"] = selected.cy.baseVal.value;
				changes["r"] = selected.r.baseVal.value;
				selected.cx.baseVal.value = remapx(selected.cx.baseVal.value);
				selected.cy.baseVal.value = remapy(selected.cy.baseVal.value);
				// take the minimum of the new selected box's dimensions for the new circle radius
				selected.r.baseVal.value = Math.min(selectedBBox.width/2,selectedBBox.height/2);
				break;
			case "ellipse":
				changes["cx"] = selected.cx.baseVal.value;
				changes["cy"] = selected.cy.baseVal.value;
				changes["rx"] = selected.rx.baseVal.value;
				changes["ry"] = selected.ry.baseVal.value;
				selected.cx.baseVal.value = remapx(selected.cx.baseVal.value);
				selected.cy.baseVal.value = remapy(selected.cy.baseVal.value);
				selected.rx.baseVal.value = scalew(selected.rx.baseVal.value);
				selected.ry.baseVal.value = scaleh(selected.ry.baseVal.value);
				break;
			case "text":
				// cannot use x.baseVal.value here because x is a SVGLengthList
				changes["x"] = selected.getAttribute("x");
				changes["y"] = selected.getAttribute("y");
				selected.setAttribute("x", remapx(selected.getAttribute("x")));
				selected.setAttribute("y", remapy(selected.getAttribute("y")));
				break;
			case "rect":
				changes["x"] = selected.x.baseVal.value;
				changes["y"] = selected.y.baseVal.value;
				changes["width"] = selected.width.baseVal.value;
				changes["height"] = selected.height.baseVal.value;
				selected.x.baseVal.value = remapx(selected.x.baseVal.value);
				selected.y.baseVal.value = remapy(selected.y.baseVal.value);
				selected.width.baseVal.value = scalew(selected.width.baseVal.value);
				selected.height.baseVal.value = scaleh(selected.height.baseVal.value);
				break;
			default: // rect
				console.log("Unknown shape type: " + selected.tagName);
				break;
		}
		// fire changed event
		if (changes) {
			var text = (current_resize_mode == "none" ? "position" : "size");
			addCommandToHistory(new ChangeElementCommand(selected, changes, text));
		}
		call("changed", selected);
	};

	var recalculateSelectedOutline = function() {
		var selected = selectedElements[0];
		if (selected != null && theSelector != null) {
			theSelector.resize(selectedBBox);
		}
	};

// public events
	// call this function to set a single selected element
	// call this function with null to clear the selected element
	var selectElement = function(newSelected)
	{
		var selected = selectedElements[0];	
		if (selected == newSelected) return;

		// remove selected outline from previously selected element
		if (theSelector != null) {
			selectorManager.releaseSelector(theSelector);
		}

		selectedElements[0] = newSelected;
		selected = selectedElements[0];

		if (selected != null) {
			selectedBBox = selected.getBBox();

			// the manager gives us a selector
			theSelector = selectorManager.requestSelector(selected);
			
			// recalculate size and then re-append to bottom of document
			recalculateSelectedOutline();

			// set all our current styles to the selected styles
			current_fill = selected.getAttribute("fill");
			current_fill_opacity = selected.getAttribute("fill-opacity");
			current_stroke = selected.getAttribute("stroke");
			current_stroke_opacity = selected.getAttribute("stroke-opacity");
			current_stroke_width = selected.getAttribute("stroke-width");
			current_stroke_style = selected.getAttribute("stroke-dasharray");
			if (selected.tagName == "text") {
				current_font_size = selected.getAttribute("font-size");
				current_font_family = selected.getAttribute("font-family");
			}

			// do not show resize grips on text elements
			theSelector.showGrips(selected.tagName != "text" && current_mode != "multiselect");
		}

		call("selected", selected);
	};

	// in mouseDown :
	// - when we are in a create mode, the element is added to the canvas
	//   but the action is not recorded until mouseUp
	// - when we are in select mode, select the element, remember the position
	//   and do nothing else
	var mouseDown = function(evt)
	{
{'fraction_non_alphanumeric': 0.14836434681089325, 'fraction_numerical': 0.005479974267946916, 'mean_word_length': 3.148660670159138, 'pattern_counts': {'":': 96, '<': 35, '<?xml version=': 0, '>': 19, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1261163', 'n_tokens_mistral': 16253, 'n_tokens_neox': 13567, 'n_words': 3417}