//! BSD License. www.openmicroscopy.org

//!  DO NOT EDIT THIS FILE! - Edit under src/js/*.js
//!  created by $ grunt concat

/* globals Raphael: false */
/* globals console: false */

var Line = function Line(options) {

    var self = this;
    this.manager = options.manager;
    this.paper = options.paper;

    if (options.id) {
        this._id = options.id;
    } else {
        this._id = this.manager.getRandomId();
    }
    this._x1 = options.x1;
    this._y1 = options.y1;
    this._x2 = options.x2;
    this._y2 = options.y2;
    this._strokeColor = options.strokeColor;
    this._strokeWidth = options.strokeWidth || 2;
    this.handle_wh = 6;
    this._selected = false;
    this._zoomFraction = 1;
    if (options.zoom) {
        this._zoomFraction = options.zoom / 100;
    }

    this.element = this.paper.path();
    this.element.attr({'cursor': 'pointer'});

    // Drag handling of line
    if (this.manager.canEdit) {
        this.element.drag(
            function(dx, dy) {
                // DRAG, update location and redraw
                dx = dx / self._zoomFraction;
                dy = dy / self._zoomFraction;

                var offsetX = dx - this.prevX;
                var offsetY = dy - this.prevY;
                this.prevX = dx;
                this.prevY = dy;

                // Manager handles move and redraw
                self.manager.moveSelectedShapes(offsetX, offsetY, true);
                return false;
            },
            function() {
                // START drag: note the location of all points
                self._handleMousedown();
                this.prevX = 0;
                this.prevY = 0;
                return false;
            },
            function() {
                // STOP
                // notify manager if line has moved
                if (this.prevX !== 0 || this.prevY !== 0) {
                    self.manager.notifySelectedShapesChanged();
                }
                return false;
            }
        );
    }

    this.createHandles();

    this.drawShape();
};

Line.prototype.toJson = function toJson() {
    var rv = {
        'type': 'Line',
        'x1': this._x1,
        'x2': this._x2,
        'y1': this._y1,
        'y2': this._y2,
        'strokeWidth': this._strokeWidth,
        'strokeColor': this._strokeColor
    };
    if (this._id) {
        rv.id = this._id;
    }
    return rv;
};

Line.prototype.compareCoords = function compareCoords(json) {

    var selfJson = this.toJson(),
        match = true;
    if (json.type !== selfJson.type) {
        return false;
    }
    ['x1', 'y1', 'x2', 'y2'].forEach(function(c){
        if (json[c] !== selfJson[c]) {
            match = false;
        }
    });
    return match;
};

// Useful for pasting json with an offset
Line.prototype.offsetCoords = function offsetCoords(json, dx, dy) {
    json.x1 = json.x1 + dx;
    json.y1 = json.y1 + dy;
    json.x2 = json.x2 + dx;
    json.y2 = json.y2 + dy;
    return json;
};

// Shift this shape by dx and dy
Line.prototype.offsetShape = function offsetShape(dx, dy) {
    this._x1 = this._x1 + dx;
    this._y1 = this._y1 + dy;
    this._x2 = this._x2 + dx;
    this._y2 = this._y2 + dy;
    this.drawShape();
};

// handle start of drag by selecting this shape
// if not already selected
Line.prototype._handleMousedown = function _handleMousedown() {
    if (!this._selected) {
        this.manager.selectShapes([this]);
    }
};

Line.prototype.setCoords = function setCoords(coords) {
    this._x1 = coords.x1 || this._x1;
    this._y1 = coords.y1 || this._y1;
    this._x2 = coords.x2 || this._x2;
    this._y2 = coords.y2 || this._y2;
    this.drawShape();
};

Line.prototype.getCoords = function getCoords() {
    return {'x1': this._x1,
            'y1': this._y1,
            'x2': this._x2,
            'y2': this._y2};
};

Line.prototype.setStrokeColor = function setStrokeColor(strokeColor) {
    this._strokeColor = strokeColor;
    this.drawShape();
};

Line.prototype.getStrokeColor = function getStrokeColor() {
    return this._strokeColor;
};

Line.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) {
    this._strokeWidth = strokeWidth;
    this.drawShape();
};

Line.prototype.getStrokeWidth = function getStrokeWidth() {
    return this._strokeWidth;
};

Line.prototype.destroy = function destroy() {
    this.element.remove();
    this.handles.remove();
};

Line.prototype.intersectRegion = function intersectRegion(region) {
    var path = this.manager.regionToPath(region, this._zoomFraction * 100);
    var f = this._zoomFraction,
        x = parseInt(this._x1 * f, 10),
        y = parseInt(this._y1 * f, 10);

    if (Raphael.isPointInsidePath(path, x, y)) {
        return true;
    }
    var path2 = this.getPath(),
        i = Raphael.pathIntersection(path, path2);
    return (i.length > 0);
};

Line.prototype.getPath = function getPath() {
    var f = this._zoomFraction,
        x1 = this._x1 * f,
        y1 = this._y1 * f,
        x2 = this._x2 * f,
        y2 = this._y2 * f;
    return "M" + x1 + " " + y1 + "L" + x2 + " " + y2;
};

Line.prototype.isSelected = function isSelected() {
    return this._selected;
};

Line.prototype.setZoom = function setZoom(zoom) {
    this._zoomFraction = zoom / 100;
    this.drawShape();
};

Line.prototype._getLineWidth = function _getLineWidth() {
    return this._strokeWidth;
};

Line.prototype.drawShape = function drawShape() {

    var p = this.getPath(),
        strokeColor = this._strokeColor,
        strokeW = this._getLineWidth();

    this.element.attr({'path': p,
                       'stroke': strokeColor,
                       'fill': strokeColor,
                       'stroke-width': strokeW});

    if (this.isSelected()) {
        this.element.toFront();
        this.handles.show().toFront();
    } else {
        this.handles.hide();
    }

    // update Handles
    var handleIds = this.getHandleCoords();
    var hnd, h_id, hx, hy;
    for (var h=0, l=this.handles.length; h<l; h++) {
        hnd = this.handles[h];
        h_id = hnd.h_id;
        hx = handleIds[h_id].x;
        hy = handleIds[h_id].y;
        hnd.attr({'x':hx-this.handle_wh/2, 'y':hy-this.handle_wh/2});
    }
};

Line.prototype.setSelected = function setSelected(selected) {
    this._selected = !!selected;
    this.drawShape();
};


Line.prototype.createHandles = function createHandles() {
    // ---- Create Handles -----

    var self = this,
        // map of centre-points for each handle
        handleIds = this.getHandleCoords(),
        handleAttrs = {'stroke': '#4b80f9',
                        'fill': '#fff',
                        'cursor': 'move',
                        'fill-opacity': 1.0};
    // draw handles
    self.handles = this.paper.set();
    var _handle_drag = function() {
        return function (dx, dy, mouseX, mouseY, event) {

            var x, y;
            dx = dx / self._zoomFraction;
            dy = dy / self._zoomFraction;

            // on DRAG...
            if (this.h_id === "start" || this.h_id === "middle") {
                self._x1 = this.old.x1 + dx;
                self._y1 = this.old.y1 + dy;
                // if shift, make line horizontal or vertical
                if (event.shiftKey && this.h_id === "start") {
                    x = self._x1 - self._x2;
                    y = self._y1 - self._y2;
                    if (Math.abs(x/y) > 1) {
                        self._y1 = this.old.y2;
                    } else if (Math.abs(x/y) < 1){
                        self._x1 = this.old.x2;
                    }
                }
            }
            if (this.h_id === "end" || this.h_id === "middle") {
                self._x2 = this.old.x2 + dx;
                self._y2 = this.old.y2 + dy;
                // if shift, make line horizontal or vertical
                if (event.shiftKey && this.h_id === "end") {
                    x = self._x1 - self._x2;
                    y = self._y1 - self._y2;
                    if (Math.abs(x/y) > 1) {
                        self._y2 = this.old.y1;
                    } else if (Math.abs(x/y) < 1){
                        self._x2 = this.old.x1;
                    }
                }
            }
            self.drawShape();
            return false;
        };
    };
    var _handle_drag_start = function() {
        return function () {
            // START drag: cache the starting coords of the line
            this.old = {
                'x1': self._x1,
                'x2': self._x2,
                'y1': self._y1,
                'y2': self._y2
            };
            return false;
        };
    };
    var _handle_drag_end = function() {
        return function() {
            // notify manager if line has moved
            if (self._x1 !== this.old.x1 || self._y1 !== this.old.y1 ||
                    self._x2 !== this.old.x2 || self._y2 !== this.old.y2) {
                self.manager.notifyShapesChanged([self]);
            }
            return false;
        };
    };

    var hsize = this.handle_wh,
        hx, hy, handle;
    for (var key in handleIds) {
        hx = handleIds[key].x;
        hy = handleIds[key].y;
        handle = this.paper.rect(hx-hsize/2, hy-hsize/2, hsize, hsize);
        handle.attr({'cursor': 'move'});
        handle.h_id = key;
        handle.line = self;

        if (this.manager.canEdit) {
            handle.drag(
                _handle_drag(),
                _handle_drag_start(),
                _handle_drag_end()
            );
        }
        self.handles.push(handle);
    }
    self.handles.attr(handleAttrs).hide();     // show on selection
};

Line.prototype.getHandleCoords = function getHandleCoords() {
    var f = this._zoomFraction,
        x1 = this._x1 * f,
        y1 = this._y1 * f,
        x2 = this._x2 * f,
        y2 = this._y2 * f;
    return {'start': {x: x1, y: y1},
        'middle': {x: (x1+x2)/2, y: (y1+y2)/2},
        'end': {x: x2, y: y2}
    };
};



var Arrow = function Arrow(options) {

    var that = new Line(options);

    var toJ = that.toJson;

    that.toJson = function toJson() {
        var lineJson = toJ.call(that);
        lineJson.type = "Arrow";
        return lineJson;
    };

    // Since we draw arrow by outline, always use thin line
    that._getLineWidth = function _getLineWidth() {
        return 0;
    };

    that.getPath = function getPath() {

        // We want the arrow tip to be precisely at x2, y2, so we
        // can't have a fat line at x2, y2. Instead we need to
        // trace the whole outline of the arrow with a thin line

        var zf = this._zoomFraction,
            x1 = this._x1 * zf,
            y1 = this._y1 * zf,
            x2 = this._x2 * zf,
            y2 = this._y2 * zf,
            w = this._strokeWidth * 0.5;

        var headSize = (this._strokeWidth * 4) + 5,
            dx = x2 - x1,
            dy = y2 - y1;

        var lineAngle = Math.atan(dx / dy);
        var f = (dy < 0 ? 1 : -1);

        // Angle of arrow head is 0.8 radians (0.4 either side of lineAngle)
        var arrowPoint1x = x2 + (f * Math.sin(lineAngle - 0.4) * headSize),
            arrowPoint1y = y2 + (f * Math.cos(lineAngle - 0.4) * headSize),
            arrowPoint2x = x2 + (f * Math.sin(lineAngle + 0.4) * headSize),
            arrowPoint2y = y2 + (f * Math.cos(lineAngle + 0.4) * headSize),
            arrowPointMidx = x2 + (f * Math.sin(lineAngle) * headSize * 0.5),
            arrowPointMidy = y2 + (f * Math.cos(lineAngle) * headSize * 0.5);

        var lineOffsetX = f * Math.cos(lineAngle) * w,
            lineOffsetY = f * Math.sin(lineAngle) * w,
            startLeftX = x1 - lineOffsetX,
            startLeftY = y1 + lineOffsetY,
            startRightX = x1 + lineOffsetX,
            startRightY = y1 - lineOffsetY,
            endLeftX = arrowPointMidx - lineOffsetX,
            endLeftY = arrowPointMidy + lineOffsetY,
            endRightX = arrowPointMidx + lineOffsetX,
            endRightY = arrowPointMidy - lineOffsetY;

        // Outline goes around the 'line' (starting in middle of arrowhead)
        var linePath = "M" + endRightX + " " + endRightY + " L" + endLeftX + " " + endLeftY;
        linePath = linePath + " L" + startLeftX + " " + startLeftY + " L" + startRightX + " " + startRightY;
        linePath = linePath + " L" + endRightX + " " + endRightY;

        // Then goes around the arrow head enough to fill it all in!
        var arrowPath = linePath + " L" + arrowPoint1x + " " + arrowPoint1y + " L" + arrowPoint2x + " " + arrowPoint2y;
        arrowPath = arrowPath + " L" + x2 + " " + y2 + " L" + arrowPoint1x + " " + arrowPoint1y + " L" + x2 + " " + y2;
        arrowPath = arrowPath + " L" + arrowPoint1x + " " + arrowPoint1y;
        return arrowPath;
    };

    // since we've over-ridden getPath() after it is called
    // during  new Line(options)
    // we need to call it again!
    that.drawShape();

    return that;
};



// Class for creating Lines.
var CreateLine = function CreateLine(options) {

    this.paper = options.paper;
    this.manager = options.manager;
};

CreateLine.prototype.startDrag = function startDrag(startX, startY) {

    var strokeColor = this.manager.getStrokeColor(),
        strokeWidth = this.manager.getStrokeWidth(),
        zoom = this.manager.getZoom();

    this.startX = startX;
    this.startY = startY;

    this.line = new Line({
        'manager': this.manager,
        'paper': this.paper,
        'x1': startX,
        'y1': startY,
        'x2': startX,
        'y2': startY,
        'strokeWidth': strokeWidth,
        'zoom': zoom,
        'strokeColor': strokeColor});
};

CreateLine.prototype.drag = function drag(dragX, dragY, shiftKey) {

    // if shiftKey, constrain to horizontal / vertical
    if (shiftKey) {
        var dx = this.startX - dragX,
            dy = this.startY - dragY;

        if (Math.abs(dx/dy) > 1) {
            dy = 0;
        } else {
            dx = 0;
        }
        dragX = (dx - this.startX) * -1;
        dragY = (dy - this.startY) * -1;
    }

    this.line.setCoords({'x2': dragX, 'y2': dragY});
};

CreateLine.prototype.stopDrag = function stopDrag() {

    var coords = this.line.getCoords();
    if ((Math.abs(coords.x1 - coords.x2) < 2) &&
            (Math.abs(coords.y1 - coords.y2) < 2)) {
        this.line.destroy();
        delete this.line;
        return;
    }
    // on the 'new:shape' trigger, this shape will already be selected
    this.line.setSelected(true);
    this.manager.addShape(this.line);
};


var CreateArrow = function CreateArrow(options) {

    var that = new CreateLine(options);

    that.startDrag = function startDrag(startX, startY) {
        var strokeColor = this.manager.getStrokeColor(),
            strokeWidth = this.manager.getStrokeWidth(),
            zoom = this.manager.getZoom();

        this.startX = startX;
        this.startY = startY;

        this.line = new Arrow({
            'manager': this.manager,
            'paper': this.paper,
            'x1': startX,
            'y1': startY,
            'x2': startX,
            'y2': startY,
            'strokeWidth': strokeWidth,
            'zoom': zoom,
            'strokeColor': strokeColor});
    };

    return that;
};

/* globals Raphael: false */
/* globals console: false */

var Rect = function Rect(options) {

    var self = this;
    this.paper = options.paper;
    this.manager = options.manager;

    if (options.id) {
        this._id = options.id;
    } else {
        this._id = this.manager.getRandomId();
    }
    this._x = options.x;
    this._y = options.y;
    this._width = options.width;
    this._height = options.height;
    this._strokeColor = options.strokeColor;
    this._strokeWidth = options.strokeWidth || 2;
    this._selected = false;
    this._zoomFraction = 1;
    if (options.zoom) {
        this._zoomFraction = options.zoom / 100;
    }
    this.handle_wh = 6;

    this.element = this.paper.rect();
    this.element.attr({'fill-opacity': 0.01,
                       'fill': '#fff',
                       'cursor': 'pointer'});

    if (this.manager.canEdit) {
        // Drag handling of element
        this.element.drag(
            function(dx, dy) {
                // DRAG, update location and redraw
                dx = dx / self._zoomFraction;
                dy = dy / self._zoomFraction;

                var offsetX = dx - this.prevX;
                var offsetY = dy - this.prevY;
                this.prevX = dx;
                this.prevY = dy;

                // Manager handles move and redraw
                self.manager.moveSelectedShapes(offsetX, offsetY, true);
            },
            function() {
                self._handleMousedown();
                this.prevX = 0;
                this.prevY = 0;
                return false;
            },
            function() {
                // STOP
                // notify manager if rectangle has moved
                if (this.prevX !== 0 || this.prevY !== 0) {
                    self.manager.notifySelectedShapesChanged();
                }
                return false;
            }
        );
    }

    this.createHandles();

    this.drawShape();
};

Rect.prototype.toJson = function toJson() {
    var rv = {
        'type': 'Rectangle',
        'x': this._x,
        'y': this._y,
        'width': this._width,
        'height': this._height,
        'strokeWidth': this._strokeWidth,
        'strokeColor': this._strokeColor
    };
    if (this._id) {
        rv.id = this._id;
    }
    return rv;
};

// Does this intersect a 'region' as defined by MODEL coords (not zoom dependent)
Rect.prototype.intersectRegion = function intersectRegion(region) {
    var path = this.manager.regionToPath(region, this._zoomFraction * 100);
    var f = this._zoomFraction,
        x = parseInt(this._x * f, 10),
        y = parseInt(this._y * f, 10);
    
    if (Raphael.isPointInsidePath(path, x, y)) {
        return true;
    }
    var path2 = this.getPath(),
        i = Raphael.pathIntersection(path, path2);
    return (i.length > 0);
};

// Useful for testing intersection of paths
Rect.prototype.getPath = function getPath() {

    var f = this._zoomFraction,
        x = parseInt(this._x * f, 10),
        y = parseInt(this._y * f, 10),
        width = parseInt(this._width * f, 10),
        height = parseInt(this._height * f, 10);

    var cornerPoints = [
                [x, y],
                [x + width, y],
                [x + width, y + height],
                [x, y + height]
            ];
    var path = [];
    for (var i = 0; i <= 3; i++) {
        if (i === 0) {
            path.push("M" + cornerPoints[0].join(","));
        }
        if (i < 3) {
            path.push("L" + cornerPoints[i + 1].join(","));
        } else {
            path.push("Z");
        }
    }
    return path.join(",");
};

Rect.prototype.compareCoords = function compareCoords(json) {
    if (json.type !== "Rectangle") {
        return false;
    }
    var selfJson = this.toJson(),
        match = true;
    ['x', 'y', 'width', 'height'].forEach(function(c){
        if (json[c] !== selfJson[c]) {
            match = false;
        }
    });
    return match;
};

// Useful for pasting json with an offset
Rect.prototype.offsetCoords = function offsetCoords(json, dx, dy) {
    json.x = json.x + dx;
    json.y = json.y + dy;
    return json;
};

// Shift this shape by dx and dy
Rect.prototype.offsetShape = function offsetShape(dx, dy) {
    this._x = this._x + dx;
    this._y = this._y + dy;
    this.drawShape();
};

// handle start of drag by selecting this shape
// if not already selected
Rect.prototype._handleMousedown = function _handleMousedown() {
    if (!this._selected) {
        this.manager.selectShapes([this]);
    }
};

Rect.prototype.setSelected = function setSelected(selected) {
    this._selected = !!selected;
    this.drawShape();
};

Rect.prototype.isSelected = function isSelected() {
    return this._selected;
};

Rect.prototype.setZoom = function setZoom(zoom) {
    this._zoomFraction = zoom / 100;
    this.drawShape();
};

Rect.prototype.setCoords = function setCoords(coords) {
    this._x = coords.x || this._x;
    this._y = coords.y || this._y;
    this._width = coords.width || this._width;
    this._height = coords.height || this._height;
    this.drawShape();
};

Rect.prototype.getCoords = function getCoords() {
    return {'x': this._x,
            'y': this._y,
            'width': this._width,
            'height': this._height};
};

Rect.prototype.setStrokeColor = function setStrokeColor(strokeColor) {
    this._strokeColor = strokeColor;
    this.drawShape();
};

Rect.prototype.getStrokeColor = function getStrokeColor() {
    return this._strokeColor;
};

Rect.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) {
    this._strokeWidth = strokeWidth;
    this.drawShape();
};

Rect.prototype.getStrokeWidth = function getStrokeWidth() {
    return this._strokeWidth;
};

Rect.prototype.destroy = function destroy() {
    this.element.remove();
    this.handles.remove();
};

Rect.prototype.drawShape = function drawShape() {

    var strokeColor = this._strokeColor,
        lineW = this._strokeWidth;

    var f = this._zoomFraction,
        x = this._x * f,
        y = this._y * f,
        w = this._width * f,
        h = this._height * f;

    this.element.attr({'x':x, 'y':y,
                       'width':w, 'height':h,
                       'stroke': strokeColor,
                       'stroke-width': lineW});

    if (this.isSelected()) {
        this.element.toFront();
        this.handles.show().toFront();
    } else {
        this.handles.hide();
    }

    // update Handles
    var handleIds = this.getHandleCoords();
    var hnd, h_id, hx, hy;
    for (var i=0, l=this.handles.length; i<l; i++) {
        hnd = this.handles[i];
        h_id = hnd.h_id;
        hx = handleIds[h_id][0];
        hy = handleIds[h_id][1];
        hnd.attr({'x':hx-this.handle_wh/2, 'y':hy-this.handle_wh/2});
    }
};

Rect.prototype.getHandleCoords = function getHandleCoords() {

    var f = this._zoomFraction,
        x = this._x * f,
        y = this._y * f,
        w = this._width * f,
        h = this._height * f;

    var handleIds = {'nw': [x, y],
        'n': [x + w/2,y],
        'ne': [x + w,y],
        'w': [x, y + h/2],
        'e': [x + w, y + h/2],
        'sw': [x, y + h],
        's': [x + w/2, y + h],
        'se': [x + w, y + h]};
    return handleIds;
};

// ---- Create Handles -----
Rect.prototype.createHandles = function createHandles() {

    var self = this,
        handle_attrs = {'stroke': '#4b80f9',
                        'fill': '#fff',
                        'cursor': 'default',
                        'fill-opacity': 1.0};

    // map of centre-points for each handle
    var handleIds = this.getHandleCoords();

    // draw handles
    self.handles = this.paper.set();
    var _handle_drag = function() {
        return function (dx, dy, mouseX, mouseY, event) {

            dx = dx / self._zoomFraction;
            dy = dy / self._zoomFraction;

            // If drag on corner handle, retain aspect ratio. dx/dy = aspect
            var keep_ratio = self.fixed_ratio || event.shiftKey;
            if (keep_ratio && this.h_id.length === 2) {     // E.g. handle is corner 'ne' etc
                if (this.h_id === 'se' || this.h_id === 'nw') {
                    if (Math.abs(dx/dy) > this.aspect) {
                        dy = dx/this.aspect;
                    } else {
                        dx = dy*this.aspect;
                    }
                } else {
                    if (Math.abs(dx/dy) > this.aspect) {
                        dy = -dx/this.aspect;
                    } else {
                        dx = -dy*this.aspect;
                    }
                }
            }
            // Use dx & dy to update the location of the handle and the corresponding point of the parent
            var new_x = this.ox + dx;
            var new_y = this.oy + dy;
            var newRect = {
                x: self._x,
                y: self._y,
                width: self._width,
                height: self._height
            };
            if (this.h_id.indexOf('e') > -1) {    // if we're dragging an 'EAST' handle, update width
                newRect.width = new_x - self._x + self.handle_wh/2;
            }
            if (this.h_id.indexOf('s') > -1) {    // if we're dragging an 'SOUTH' handle, update height
                newRect.height = new_y - self._y + self.handle_wh/2;
            }
            if (this.h_id.indexOf('n') > -1) {    // if we're dragging an 'NORTH' handle, update y and height
                newRect.y = new_y + self.handle_wh/2;
                newRect.height = this.oheight - dy;
            }
            if (this.h_id.indexOf('w') > -1) {    // if we're dragging an 'WEST' handle, update x and width
                newRect.x = new_x + self.handle_wh/2;
                newRect.width = this.owidth - dx;
            }
            // Don't allow zero sized rect.
            if (newRect.width < 1 || newRect.height < 1) {
                return false;
            }

            self._x = newRect.x;
            self._y = newRect.y;
            self._width = newRect.width;
            self._height = newRect.height;
            self.drawShape();
            return false;
        };
    };
    var _handle_drag_start = function() {
        return function () {
            // START drag: simply note the location we started
            this.ox = this.attr("x") / self._zoomFraction;
            this.oy = this.attr("y") / self._zoomFraction;
            this.owidth = self._width;
            this.oheight = self._height;
            this.aspect = self._width / self._height;
            return false;
        };
    };
    var _handle_drag_end = function() {
        return function() {
            if (this.owidth !== self._width || this.oheight !== self._height) {
                self.manager.notifyShapesChanged([self]);
            }
            return false;
        };
    };
    // var _stop_event_propagation = function(e) {
    //     e.stopImmediatePropagation();
    // }
    for (var key in handleIds) {
        var hx = handleIds[key][0];
        var hy = handleIds[key][1];
        var handle = this.paper.rect(hx-self.handle_wh/2, hy-self.handle_wh/2, self.handle_wh, self.handle_wh).attr(handle_attrs);
        handle.attr({'cursor': key + '-resize'});     // css, E.g. ne-resize
        handle.h_id = key;
        handle.rect = self;

        if (self.manager.canEdit) {
            handle.drag(
                _handle_drag(),
                _handle_drag_start(),
                _handle_drag_end()
            );
        }
        // handle.mousedown(_stop_event_propagation);
        self.handles.push(handle);
    }
    self.handles.hide();     // show on selection
};



// Class for creating Lines.
var CreateRect = function CreateRect(options) {

    this.paper = options.paper;
    this.manager = options.manager;
};

CreateRect.prototype.startDrag = function startDrag(startX, startY) {

    var strokeColor = this.manager.getStrokeColor(),
        strokeWidth = this.manager.getStrokeWidth(),
        zoom = this.manager.getZoom();
    // Also need to get strokeWidth and zoom/size etc.

    this.startX = startX;
    this.startY = startY;

    this.rect = new Rect({
        'manager': this.manager,
        'paper': this.paper,
        'x': startX,
        'y': startY,
        'width': 0,
        'height': 0,
        'strokeWidth': strokeWidth,
        'zoom': zoom,
        'strokeColor': strokeColor});
};

CreateRect.prototype.drag = function drag(dragX, dragY, shiftKey) {

    var dx = this.startX - dragX,
        dy = this.startY - dragY;

    // if shiftKey, constrain to a square
    if (shiftKey) {
        if (dx * dy > 0) {
            if (Math.abs(dx/dy) > 1) {
                dy = dx;
            } else {
                dx = dy;
            }
        } else {
            if (Math.abs(dx/dy) > 1) {
                dy = -dx;
            } else {
                dx = -dy;
            }
        }
        dragX = (dx - this.startX) * -1;
        dragY = (dy - this.startY) * -1;
    }

    this.rect.setCoords({'x': Math.min(dragX, this.startX),
                        'y': Math.min(dragY, this.startY),
                        'width': Math.abs(dx), 'height': Math.abs(dy)});
};

CreateRect.prototype.stopDrag = function stopDrag() {

    var coords = this.rect.getCoords();
    if (coords.width < 2 || coords.height < 2) {
        this.rect.destroy();
        delete this.rect;
        return;
    }
    // on the 'new:shape' trigger, this shape will already be selected
    this.rect.setSelected(true);
    this.manager.addShape(this.rect);
};

/* globals Raphael: false */
/* globals console: false */

var Ellipse = function Ellipse(options) {

    var self = this;
    this.manager = options.manager;
    this.paper = options.paper;

    if (options.id) {
        this._id = options.id;
    } else {
        this._id = this.manager.getRandomId();
    }
    this._x = options.x;
    this._y = options.y;
    this._radiusX = options.radiusX;
    this._radiusY = options.radiusY;
    this._rotation = options.rotation || 0;

    // We handle transform matrix by creating this.Matrix
    // This is used as a one-off transform of the handles positions
    // when they are created. This then updates the _x, _y, _radiusX, _radiusY & rotation
    // of the ellipse itself (see below)
    if (options.transform && options.transform.startsWith('matrix')) {
        var tt = options.transform.replace('matrix(', '').replace(')', '').split(" ");
        var a1 = parseFloat(tt[0]);
        var a2 = parseFloat(tt[1]);
        var b1 = parseFloat(tt[2]);
        var b2 = parseFloat(tt[3]);
        var c1 = parseFloat(tt[4]);
        var c2 = parseFloat(tt[5]);
        this.Matrix = Raphael.matrix(a1, a2, b1, b2, c1, c2);
    }

    if (this._radiusX === 0 || this._radiusY === 0) {
        this._yxRatio = 0.5;
    } else {
        this._yxRatio = this._radiusY / this._radiusX;
    }

    this._strokeColor = options.strokeColor;
    this._strokeWidth = options.strokeWidth || 2;
    this._selected = false;
    this._zoomFraction = 1;
    if (options.zoom) {
        this._zoomFraction = options.zoom / 100;
    }
    this.handle_wh = 6;

    this.element = this.paper.ellipse();
    this.element.attr({'fill-opacity': 0.01,
                        'fill': '#fff',
                        'cursor': 'pointer'});

    // Drag handling of ellipse
    if (this.manager.canEdit) {
        this.element.drag(
            function(dx, dy) {
                // DRAG, update location and redraw
                dx = dx / self._zoomFraction;
                dy = dy / self._zoomFraction;

                var offsetX = dx - this.prevX;
                var offsetY = dy - this.prevY;
                this.prevX = dx;
                this.prevY = dy;

                // Manager handles move and redraw
                self.manager.moveSelectedShapes(offsetX, offsetY, true);
                return false;
            },
            function() {
                // START drag: note the start location
                self._handleMousedown();
                this.prevX = 0;
                this.prevY = 0;
                return false;
            },
            function() {
                // STOP
                // notify changed if moved
                if (this.prevX !== 0 || this.prevY !== 0) {
                    self.manager.notifySelectedShapesChanged();
                }
                return false;
            }
        );
    }

    // create handles, applying this.Matrix if set
    this.createHandles();
    // update x, y, radiusX, radiusY & rotation
    // If we have Matrix, recalculate width/height ratio based on all handles
    var resizeWidth = !!this.Matrix;
    this.updateShapeFromHandles(resizeWidth);
    // and draw the Ellipse
    this.drawShape();
};

Ellipse.prototype.toJson = function toJson() {
    var rv = {
        'type': "Ellipse",
        'x': this._x,
        'y': this._y,
        'radiusX': this._radiusX,
        'radiusY': this._radiusY,
        'rotation': this._rotation,
        'strokeWidth': this._strokeWidth,
        'strokeColor': this._strokeColor
    };
    if (this._id) {
        rv.id = this._id;
    }
    return rv;
};

Ellipse.prototype.compareCoords = function compareCoords(json) {

    var selfJson = this.toJson(),
        match = true;
    if (json.type !== selfJson.type) {
        return false;
    }
    ['x', 'y', 'radiusX', 'radiusY', 'rotation'].forEach(function(c){
        if (Math.round(json[c]) !== Math.round(selfJson[c])) {
            match = false;
        }
    });
    return match;
};

// Useful for pasting json with an offset
Ellipse.prototype.offsetCoords = function offsetCoords(json, dx, dy) {
    json.x = json.x + dx;
    json.y = json.y + dy;
    return json;
};

// Shift this shape by dx and dy
Ellipse.prototype.offsetShape = function offsetShape(dx, dy) {
    this._x = this._x + dx;
    this._y = this._y + dy;
    this.drawShape();
};

// handle start of drag by selecting this shape
// if not already selected
Ellipse.prototype._handleMousedown = function _handleMousedown() {
    if (!this._selected) {
        this.manager.selectShapes([this]);
    }
};

Ellipse.prototype.setColor = function setColor(strokeColor) {
    this._strokeColor = strokeColor;
    this.drawShape();
};

Ellipse.prototype.getStrokeColor = function getStrokeColor() {
    return this._strokeColor;
};

Ellipse.prototype.setStrokeColor = function setStrokeColor(strokeColor) {
    this._strokeColor = strokeColor;
    this.drawShape();
};

Ellipse.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) {
    this._strokeWidth = strokeWidth;
    this.drawShape();
};

Ellipse.prototype.getStrokeWidth = function getStrokeWidth() {
    return this._strokeWidth;
};

Ellipse.prototype.destroy = function destroy() {
    this.element.remove();
    this.handles.remove();
};

Ellipse.prototype.intersectRegion = function intersectRegion(region) {
    var path = this.manager.regionToPath(region, this._zoomFraction * 100);
    var f = this._zoomFraction,
        x = parseInt(this._x * f, 10),
        y = parseInt(this._y * f, 10);

    if (Raphael.isPointInsidePath(path, x, y)) {
        return true;
    }
    var path2 = this.getPath(),
        i = Raphael.pathIntersection(path, path2);
    return (i.length > 0);
};

Ellipse.prototype.getPath = function getPath() {

    // Adapted from https://github.com/poilu/raphael-boolean
    var a = this.element.attrs,
        radiusX = a.radiusX,
        radiusY = a.radiusY,
        cornerPoints = [
            [a.x - radiusX, a.y - radiusY],
            [a.x + radiusX, a.y - radiusY],
            [a.x + radiusX, a.y + radiusY],
            [a.x - radiusX, a.y + radiusY]
        ],
        path = [];
    var radiusShift = [
        [
            [0, 1],
            [1, 0]
        ],
        [
            [-1, 0],
            [0, 1]
        ],
        [
            [0, -1],
            [-1, 0]
        ],
        [
            [1, 0],
            [0, -1]
        ]
    ];

    //iterate all corners
    for (var i = 0; i <= 3; i++) {
        //insert starting point
        if (i === 0) {
            path.push(["M", cornerPoints[0][0], cornerPoints[0][1] + radiusY]);
        }

        //insert "curveto" (radius factor .446 is taken from Inkscape)
        var c1 = [cornerPoints[i][0] + radiusShift[i][0][0] * radiusX * 0.446, cornerPoints[i][1] + radiusShift[i][0][1] * radiusY * 0.446];
        var c2 = [cornerPoints[i][0] + radiusShift[i][1][0] * radiusX * 0.446, cornerPoints[i][1] + radiusShift[i][1][1] * radiusY * 0.446];
        var p2 = [cornerPoints[i][0] + radiusShift[i][1][0] * radiusX, cornerPoints[i][1] + radiusShift[i][1][1] * radiusY];
        path.push(["C", c1[0], c1[1], c2[0], c2[1], p2[0], p2[1]]);
    }
    path.push(["Z"]);
    path = path.join(",").replace(/,?([achlmqrstvxz]),?/gi, "$1");

    if (this._rotation !== 0) {
        path = Raphael.transformPath(path, "r" + this._rotation);
    }
    return path;
};

Ellipse.prototype.isSelected = function isSelected() {
    return this._selected;
};

Ellipse.prototype.setZoom = function setZoom(zoom) {
    this._zoomFraction = zoom / 100;
    this.drawShape();
};

Ellipse.prototype.updateHandle = function updateHandle(handleId, x, y, shiftKey) {
    // Refresh the handle coordinates, then update the specified handle
    // using MODEL coordinates
    this._handleIds = this.getHandleCoords();
    var h = this._handleIds[handleId];
    h.x = x;
    h.y = y;
    var resizeWidth = (handleId === "left" || handleId === "right");
    this.updateShapeFromHandles(resizeWidth, shiftKey);
};

Ellipse.prototype.updateShapeFromHandles = function updateShapeFromHandles(resizeWidth, shiftKey) {
    var hh = this._handleIds,
        lengthX = hh.end.x - hh.start.x,
        lengthY = hh.end.y - hh.start.y,
        widthX = hh.left.x - hh.right.x,
        widthY = hh.left.y - hh.right.y,
        rot;
    // Use the 'start' and 'end' handles to get rotation and length
    if (lengthX === 0){
        this._rotation = 90;
    } else if (lengthX > 0) {
        rot = Math.atan(lengthY / lengthX);
        this._rotation = Raphael.deg(rot);
    } else if (lengthX < 0) {
        rot = Math.atan(lengthY / lengthX);
        this._rotation = 180 + Raphael.deg(rot);
    }
    
    // centre is half-way between 'start' and 'end' handles
    this._x = (hh.start.x + hh.end.x)/2;
    this._y = (hh.start.y + hh.end.y)/2;
    // Radius-x is half of distance between handles
    this._radiusX = Math.sqrt((lengthX * lengthX) + (lengthY * lengthY)) / 2;
    // Radius-y may depend on handles OR on x/y ratio
    if (resizeWidth) {
        this._radiusY = Math.sqrt((widthX * widthX) + (widthY * widthY)) / 2;
        this._yxRatio = this._radiusY / this._radiusX;
    } else {
        if (shiftKey) {
            this._yxRatio = 1;
        }
        this._radiusY = this._yxRatio * this._radiusX;
    }

    this.drawShape();
};

Ellipse.prototype.drawShape = function drawShape() {

    var strokeColor = this._strokeColor,
        strokeW = this._strokeWidth;

    var f = this._zoomFraction,
        x = this._x * f,
        y = this._y * f,
        radiusX = this._radiusX * f,
        radiusY = this._radiusY * f;

    this.element.attr({'cx': x,
                       'cy': y,
                       'rx': radiusX,
                       'ry': radiusY,
                       'stroke': strokeColor,
                       'stroke-width': strokeW});
    this.element.transform('r'+ this._rotation);

    if (this.isSelected()) {
        this.element.toFront();
        this.handles.show().toFront();
    } else {
        this.handles.hide();
    }

    // handles have been updated (model coords)
    this._handleIds = this.getHandleCoords();
    var hnd, h_id, hx, hy;
    for (var h=0, l=this.handles.length; h<l; h++) {
        hnd = this.handles[h];
        h_id = hnd.h_id;
        hx = this._handleIds[h_id].x * this._zoomFraction;
        hy = this._handleIds[h_id].y * this._zoomFraction;
        hnd.attr({'x':hx-this.handle_wh/2, 'y':hy-this.handle_wh/2});
    }
};

Ellipse.prototype.setSelected = function setSelected(selected) {
    this._selected = !!selected;
    this.drawShape();
};


Ellipse.prototype.createHandles = function createHandles() {
    // ---- Create Handles -----

    // NB: handleIds are used to calculate ellipse coords
    // so handledIds are scaled to MODEL coords, not zoomed.
    this._handleIds = this.getHandleCoords();

    var self = this,
        // map of centre-points for each handle
        handleAttrs = {'stroke': '#4b80f9',
                        'fill': '#fff',
                        'cursor': 'move',
                        'fill-opacity': 1.0};

    // draw handles
    self.handles = this.paper.set();
    var _handle_drag = function() {
        return function (dx, dy, mouseX, mouseY, event) {
            dx = dx / self._zoomFraction;
            dy = dy / self._zoomFraction;
            // on DRAG...
            var absX = dx + this.ox,
                absY = dy + this.oy;
            self.updateHandle(this.h_id, absX, absY, event.shiftKey);
            return false;
        };
    };
    var _handle_drag_start = function() {
        return function () {
            // START drag: simply note the location we started
            // we scale by zoom to get the 'model' coordinates
            this.ox = (this.attr("x") + this.attr('width')/2) / self._zoomFraction;
            this.oy = (this.attr("y") + this.attr('height')/2) / self._zoomFraction;
            return false;
        };
    };
    var _handle_drag_end = function() {
        return function() {
            // simply notify manager that shape has changed
            self.manager.notifyShapesChanged([self]);
            return false;
        };
    };

    var hsize = this.handle_wh,
        hx, hy, handle;
    for (var key in this._handleIds) {
        hx = this._handleIds[key].x;
        hy = this._handleIds[key].y;
        // If we have a transformation matrix, apply it...
        if (this.Matrix) {
            var matrixStr = this.Matrix.toTransformString();
            // Matrix that only contains rotation and translation 
            // E.g. t111.894472287,-140.195845758r32.881,0,0  Will be handled correctly:
            // Resulting handles position and x,y radii will be calculated
            // so we don't need to apply transform to ellipse itself
            // BUT, if we have other transforms such as skew, we can't do this.
            // Best to just show warning if Raphael can't resolve matrix to simpler transforms:
            // E.g. m2.39,-0.6,2.1,0.7,-1006,153 
            if (matrixStr.indexOf('m') > -1) {
                console.log("Matrix only supports rotation & translation. " + matrixStr + " may contain skew for shape: ", this.toJson());
            }
            var mx = this.Matrix.x(hx, hy);
            var my = this.Matrix.y(hx, hy);
            hx = mx;
            hy = my;
            // update the source coordinates
            this._handleIds[key].x = hx;
            this._handleIds[key].y = hy;
        }
        handle = this.paper.rect(hx-hsize/2, hy-hsize/2, hsize, hsize);
        handle.attr({'cursor': 'move'});
        handle.h_id = key;
        handle.line = self;

        if (this.manager.canEdit) {
            handle.drag(
                _handle_drag(),
                _handle_drag_start(),
                _handle_drag_end()
            );
        }
        self.handles.push(handle);
    }

    self.handles.attr(handleAttrs).hide();     // show on selection
};

Ellipse.prototype.getHandleCoords = function getHandleCoords() {
    // Returns MODEL coordinates (not zoom coordinates)
    var rot = Raphael.rad(this._rotation),
        x = this._x,
        y = this._y,
        radiusX = this._radiusX,
        radiusY = this._radiusY,
        startX = x - (Math.cos(rot) * radiusX),
        startY = y - (Math.sin(rot) * radiusX),
        endX = x + (Math.cos(rot) * radiusX),
        endY = y + (Math.sin(rot) * radiusX),
        leftX = x + (Math.sin(rot) * radiusY),
        leftY = y - (Math.cos(rot) * radiusY),
        rightX = x - (Math.sin(rot) * radiusY),
        rightY = y + (Math.cos(rot) * radiusY);

    return {'start':{x: startX, y: startY},
            'end':{x: endX, y: endY},
            'left':{x: leftX, y: leftY},
            'right':{x: rightX, y: rightY}
        };
};


// Class for creating Lines.
var CreateEllipse = function CreateEllipse(options) {

    this.paper = options.paper;
    this.manager = options.manager;
};

CreateEllipse.prototype.startDrag = function startDrag(startX, startY) {

    var strokeColor = this.manager.getStrokeColor(),
        strokeWidth = this.manager.getStrokeWidth(),
        zoom = this.manager.getZoom();

    this.ellipse = new Ellipse({
        'manager': this.manager,
        'paper': this.paper,
        'x': startX,
        'y': startY,
        'radiusX': 0,
        'radiusY': 0,
        'rotation': 0,
        'strokeWidth': strokeWidth,
        'zoom': zoom,
        'strokeColor': strokeColor});
};

CreateEllipse.prototype.drag = function drag(dragX, dragY, shiftKey) {

    this.ellipse.updateHandle('end', dragX, dragY, shiftKey);
};

CreateEllipse.prototype.stopDrag = function stopDrag() {

    // Don't create ellipse of zero size (click, without drag)
    var coords = this.ellipse.toJson();
    if (coords.radiusX < 2) {
        this.ellipse.destroy();
        delete this.ellipse;
        return;
    }
    // on the 'new:shape' trigger, this shape will already be selected
    this.ellipse.setSelected(true);
    this.manager.addShape(this.ellipse);
};

/* globals Raphael: false */
/* globals console: false */

var Polygon = function Polygon(options) {

    var self = this;
    this.manager = options.manager;
    this.paper = options.paper;

    if (options.id) {
        this._id = options.id;
    } else {
        this._id = this.manager.getRandomId();
    }
    this._points = options.points;

    this._strokeColor = options.strokeColor;
    this._strokeWidth = options.strokeWidth || 2;
    this._selected = false;
    this._zoomFraction = 1;
    if (options.zoom) {
        this._zoomFraction = options.zoom / 100;
    }
    this.handle_wh = 6;

    this.element = this.paper.path("");
    this.element.attr({'fill-opacity': 0.01,
                        'fill': '#fff',
                        'cursor': 'pointer'});

    if (this.manager.canEdit) {
        // Drag handling of element
        this.element.drag(
            function(dx, dy) {
                if (self._zoomFraction === 0) {
                    return;     // just in case
                }
                // DRAG, update location and redraw
                dx = dx / self._zoomFraction;
                dy = dy / self._zoomFraction;

                var offsetX = dx - this.prevX;
                var offsetY = dy - this.prevY;
                this.prevX = dx;
                this.prevY = dy;

                // Manager handles move and redraw
                self.manager.moveSelectedShapes(offsetX, offsetY, true);
            },
            function() {
                self._handleMousedown();
                this.prevX = 0;
                this.prevY = 0;
                return false;
            },
            function() {
                // STOP
                // notify manager if rectangle has moved
                if (this.prevX !== 0 || this.prevY !== 0) {
                    self.manager.notifySelectedShapesChanged();
                }
                return false;
            }
        );
    }

    // create handles...
    this.createHandles();
    // and draw the Polygon
    this.drawShape();
};

Polygon.prototype.toJson = function toJson() {
    var rv = {
        'type': "Polygon",
        'points': this._points,
        'strokeWidth': this._strokeWidth,
        'strokeColor': this._strokeColor
    };
    if (this._id) {
        rv.id = this._id;
    }
    return rv;
};

Polygon.prototype.compareCoords = function compareCoords(json) {

    var selfJson = this.toJson(),
        match = true;
    if (json.type !== selfJson.type) {
        return false;
    }
    return json.points === selfJson.points;
};

// Useful for pasting json with an offset
Polygon.prototype.offsetCoords = function offsetCoords(json, dx, dy) {
    json.points = json.points.split(" ").map(function(xy){
        return xy.split(",").map(function(c, i){
            return parseFloat(c, 10) + [dx, dy][i]
        }).join(",")
    }).join(" ");
    return json;
};

// Shift this shape by dx and dy
Polygon.prototype.offsetShape = function offsetShape(dx, dy) {
    // Offset all coords in points string "229,171 195,214 195,265 233,33"
    var points = this._points.split(" ").map(function(xy){
        return xy.split(",").map(function(c, i){
            return parseFloat(c, 10) + [dx, dy][i]
        }).join(",")
    }).join(" ");
    this._points = points;
    this.drawShape();
};

// handle start of drag by selecting this shape
// if not already selected
Polygon.prototype._handleMousedown = function _handleMousedown() {
    if (!this._selected) {
        this.manager.selectShapes([this]);
    }
};

Polygon.prototype.setColor = function setColor(strokeColor) {
    this._strokeColor = strokeColor;
    this.drawShape();
};

Polygon.prototype.getStrokeColor = function getStrokeColor() {
    return this._strokeColor;
};

Polygon.prototype.setStrokeColor = function setStrokeColor(strokeColor) {
    this._strokeColor = strokeColor;
    this.drawShape();
};

Polygon.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) {
    this._strokeWidth = strokeWidth;
    this.drawShape();
};

Polygon.prototype.getStrokeWidth = function getStrokeWidth() {
    return this._strokeWidth;
};

Polygon.prototype.destroy = function destroy() {
    this.element.remove();
    this.handles.remove();
};

Polygon.prototype.intersectRegion = function intersectRegion(region) {
    // region is {x, y, width, height} - Model coords (not zoomed)
    // Compare with model coords of points...

    // Get bounding box from points...
    var coords = this._points.split(" ").reduce(function(prev, xy){
        var x = parseInt(xy.split(',')[0], 10);
        var y = parseInt(xy.split(',')[1], 10);
        if (!prev) {
            prev = {'min_x': x, 'min_y': y, 'max_x': x, 'max_y': y};
        } else {
            prev.min_x = Math.min(prev.min_x, x);
            prev.min_y = Math.min(prev.min_y, y);
            prev.max_x = Math.max(prev.max_x, x);
            prev.max_y = Math.max(prev.max_y, y);
        }
        return prev;
    }, undefined);

    // check for overlap - NB: this may return True even if no intersection
    // since Polygon doesn't fill it's bounding box
    if (coords.min_x > (region.x + region.width) ||
        coords.min_y > (region.y + region.height) ||
        coords.max_x < region.x ||
        coords.max_y < region.y) {
        return false;
    }
    return true;
};

Polygon.prototype.getPath = function getPath() {
    // Convert points string "229,171 195,214 195,265 233,33"
    // to Raphael path "M229,171L195,214L195,265L233,33Z"
    // Handles scaling by zoomFraction
    var f = this._zoomFraction;
    var path = this._points.split(" ").map(function(xy){
        return xy.split(",").map(function(c){return parseInt(c, 10) * f}).join(",");
    }).join("L");
    path = "M" + path + "Z";
    return path;
};

Polygon.prototype.isSelected = function isSelected() {
    return this._selected;
};

Polygon.prototype.setZoom = function setZoom(zoom) {
    this._zoomFraction = zoom / 100;
    this.drawShape();
};

Polygon.prototype.updateHandle = function updateHandle(handleIndex, x, y, shiftKey) {
    var coords = this._points.split(" ");
    coords[handleIndex] = x + "," + y;
    this._points = coords.join(" ");
};

Polygon.prototype.drawShape = function drawShape() {

    var strokeColor = this._strokeColor,
        strokeW = this._strokeWidth;

    var f = this._zoomFraction;
    var path = this.getPath();

    this.element.attr({'path': path,
                       'stroke': strokeColor,
                       'stroke-width': strokeW});

    if (this.isSelected()) {
        this.element.toFront();
        this.handles.show().toFront();
    } else {
        this.handles.hide();
    }

    // handles have been updated (model coords)
    var hnd, hx, hy;
    this._points.split(" ").forEach(function(xy, i){
        var xy = xy.split(",");
        hx = parseInt(xy[0]) * this._zoomFraction;
        hy = parseInt(xy[1]) * this._zoomFraction;
        hnd = this.handles[i];
        hnd.attr({'x':hx-this.handle_wh/2, 'y':hy-this.handle_wh/2});
    }.bind(this));
};

Polygon.prototype.setSelected = function setSelected(selected) {
    this._selected = !!selected;
    this.drawShape();
};


Polygon.prototype.createHandles = function createHandles() {
    // ---- Create Handles -----

    // NB: handleIds are used to calculate coords
    // so handledIds are scaled to MODEL coords, not zoomed.

    var self = this,
        // map of centre-points for each handle
        handleAttrs = {'stroke': '#4b80f9',
                        'fill': '#fff',
                        'cursor': 'move',
                        'fill-opacity': 1.0};

    // draw handles
    self.handles = this.paper.set();
    var _handle_drag = function() {
        return function (dx, dy, mouseX, mouseY, event) {
            dx = dx / self._zoomFraction;
            dy = dy / self._zoomFraction;
            // on DRAG...
            var absX = dx + this.ox,
                absY = dy + this.oy;
            self.updateHandle(this.h_id, absX, absY, event.shiftKey);
            self.drawShape();
            return false;
        };
    };
    var _handle_drag_start = function() {
        return function () {
            // START drag: simply note the location we started
            // we scale by zoom to get the 'model' coordinates
            this.ox = (this.attr("x") + this.attr('width')/2) / self._zoomFraction;
            this.oy = (this.attr("y") + this.attr('height')/2) / self._zoomFraction;
            return false;
        };
    };
    var _handle_drag_end = function() {
        return function() {
            // simply notify manager that shape has changed
            self.manager.notifyShapesChanged([self]);
            return false;
        };
    };

    var hsize = this.handle_wh,
        hx, hy, handle;
    this._points.split(" ").forEach(function(xy, i){
        var xy = xy.split(",");
        hx = parseInt(xy[0]);
        hy = parseInt(xy[1]);

        handle = self.paper.rect(hx-hsize/2, hy-hsize/2, hsize, hsize);
        handle.attr({'cursor': 'move'});
        handle.h_id = i;

        if (self.manager.canEdit) {
            handle.drag(
                _handle_drag(),
                _handle_drag_start(),
                _handle_drag_end()
            );
        }
        self.handles.push(handle);
    });

    self.handles.attr(handleAttrs).hide();     // show on selection
};


var Polyline = function Polyline(options) {
    var that = new Polygon(options);

    var toJ = that.toJson;
    that.toJson = function toJson() {
        var shapeJson = toJ.call(that);
        shapeJson.type = "Polyline";
        return shapeJson;
    };

    var getPolygonPath = that.getPath;
    that.getPath = function getPath() {
        var polygonPath = getPolygonPath.call(that);
        return polygonPath.replace("Z", "");
    }

    // since we've over-ridden getPath() after it is called
    // during  new Polygon(options)
    // we need to call it again!
    that.drawShape();
    return that;
}

/* globals Raphael: false */
/* globals CreateRect: false */
/* globals Rect: false */
/* globals CreateLine: false */
/* globals Line: false */
/* globals CreateArrow: false */
/* globals Arrow: false */
/* globals CreateEllipse: false */
/* globals Ellipse: false */
/* globals Polygon: false */
/* globals Polyline: false */
/* globals console: false */

var ShapeManager = function ShapeManager(elementId, width, height, options) {

    var self = this;
    options = options || {};

    // Keep track of state, strokeColor etc
    this.STATES = ["SELECT", "RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"];
    this._state = "SELECT";
    this._strokeColor = "#ff0000";
    this._strokeWidth = 2;
    this._orig_width = width;
    this._orig_height = height;
    this._zoom = 100;
    // Don't allow editing of shapes - no drag/click events
    this.canEdit = !options.readOnly;

    // Set up Raphael paper...
    this.paper = Raphael(elementId, width, height);

    // jQuery element used for .offset() etc.
    this.$el = $("#" + elementId);

    // Store all the shapes we create
    this._shapes = [];

    // Add a full-size background to cover existing shapes while
    // we're creating new shapes, to stop them being selected.
    // Mouse events on this will bubble up to svg and are handled below
    this.newShapeBg = this.paper.rect(0, 0, width, height);
    this.newShapeBg.attr({'fill':'#000',
                          'fill-opacity':0.01,
                          'stroke-width': 0,
                          'cursor': 'default'});
    this.selectRegion = this.paper.rect(0, 0, width, height);
    this.selectRegion.hide().attr({'stroke': '#ddd',
                                   'stroke-width': 0,
                                   'stroke-dasharray': '- '});
    if (this.canEdit) {
        this.newShapeBg.drag(
            function(){
                self.drag.apply(self, arguments);
            },
            function(){
                self.startDrag.apply(self, arguments);
            },
            function(){
                self.stopDrag.apply(self, arguments);
            });

        this.shapeFactories = {
            "RECT": new CreateRect({'manager': this, 'paper': this.paper}),
            "ELLIPSE": new CreateEllipse({'manager': this, 'paper': this.paper}),
            "LINE": new CreateLine({'manager': this, 'paper': this.paper}),
            "ARROW": new CreateArrow({'manager': this, 'paper': this.paper}),
        };

        this.createShape = this.shapeFactories.LINE;
    } else {
        this.shapeFactories = {};
    }
};

ShapeManager.prototype.startDrag = function startDrag(x, y, event){
    // clear any existing selected shapes
    this.clearSelectedShapes();

    var offset = this.$el.offset(),
        startX = x - offset.left,
        startY = y - offset.top;

    if (this.getState() === "SELECT") {

        this._dragStart = {x: startX, y: startY};

        this.selectRegion.attr({'x': startX,
                                'y': startY,
                                'width': 0,
                                'height': 0});
        this.selectRegion.toFront().show();

    } else {
        // create a new shape with X and Y
        // createShape helper can get other details itself

        // correct for zoom before passing coordinates to shape
        var zoomFraction = this._zoom / 100;
        startX = startX / zoomFraction;
        startY = startY / zoomFraction;
        this.createShape.startDrag(startX, startY);
    }

    // Move this in front of new shape so that drag events don't get lost to the new shape
    this.newShapeBg.toFront();
};

ShapeManager.prototype.drag = function drag(dx, dy, x, y, event){
    var offset = this.$el.offset(),
        dragX = x - offset.left,
        dragY = y - offset.top;

    if (this.getState() === "SELECT") {

        dx = this._dragStart.x - dragX,
        dy = this._dragStart.y - dragY;

        this.selectRegion.attr({'x': Math.min(dragX, this._dragStart.x),
                                'y': Math.min(dragY, this._dragStart.y),
                                'width': Math.abs(dx),
                                'height': Math.abs(dy)});
    } else {

        // correct for zoom before passing coordinates to shape
        var zoomFraction = this._zoom / 100,
            shiftKey = event.shiftKey;
        dragX = dragX / zoomFraction;
        dragY = dragY / zoomFraction;
        this.createShape.drag(dragX, dragY, shiftKey);
    }
};

ShapeManager.prototype.stopDrag = function stopDrag(x, y, event){
    if (this.getState() === "SELECT") {

        // need to get MODEL coords (correct for zoom)
        var region = this.selectRegion.attr(),
            f = this._zoom/100,
            sx = region.x / f,
            sy = region.y / f,
            width = region.width / f,
            height = region.height / f;
        this.selectShapesByRegion({x: sx, y: sy, width: width, height: height});

        // Hide region and move drag listening element to back again.
        this.selectRegion.hide();
        this.newShapeBg.toBack();
    } else {
        this.createShape.stopDrag();
    }
};

ShapeManager.prototype.setState = function setState(state) {
    if (this.STATES.indexOf(state) === -1) {
        console.log("Invalid state: ", state, "Needs to be in", this.STATES);
        return;
    }
    // When creating shapes, cover existing shapes with newShapeBg
    var shapes = ["RECT", "LINE", "ARROW", "ELLIPSE", "POLYGON"];
    if (shapes.indexOf(state) > -1) {
        this.newShapeBg.toFront();
        this.newShapeBg.attr({'cursor': 'crosshair'});
        // clear selected shapes
        this.clearSelectedShapes();

        if (this.shapeFactories[state]) {
            this.createShape = this.shapeFactories[state];
        }
    } else if (state === "SELECT") {
        // Used to handle drag-select events
        this.newShapeBg.toBack();
        this.newShapeBg.attr({'cursor': 'default'});
    }

    this._state = state;
};

ShapeManager.prototype.getState = function getState() {
    return this._state;
};

ShapeManager.prototype.setZoom = function setZoom(zoomPercent) {
    // var zoom = this.shapeEditor.get('zoom');

    // var $imgWrapper = $(".image_wrapper"),
    //     currWidth = $imgWrapper.width(),
    //     currHeight = $imgWrapper.height(),
    //     currTop = parseInt($imgWrapper.css('top'), 10),
    //     currLeft = parseInt($imgWrapper.css('left'), 10);

    // var width = 512 * zoom / 100,
    //     height = 512 * zoom / 100;
    // $("#shapeCanvas").css({'width': width + "px", 'height': height + "px"});

    this._zoom = zoomPercent;
    // Update the svg and our newShapeBg.
    // $("svg").css({'width': width + "px", 'height': height + "px"});
    var width = this._orig_width * zoomPercent / 100,
        height = this._orig_height * zoomPercent / 100;
    this.paper.setSize(width, height);
    this.paper.canvas.setAttribute("viewBox", "0 0 "+width+" "+height);
    this.newShapeBg.attr({'width': width, 'height': height});

    // zoom the shapes
    this._shapes.forEach(function(shape){
        shape.setZoom(zoomPercent);
    });

    // // image 
    // $(".image_wrapper").css({'width': width + "px", 'height': height + "px"});
    // // offset
    // var deltaTop = (height - currHeight) / 2,
    //     deltaLeft = (width - currWidth) / 2;
    // $(".image_wrapper").css({'left': (currLeft - deltaLeft) + "px",
    //                          'top': (currTop - deltaTop) + "px"});
};

ShapeManager.prototype.getZoom = function getZoom(zoomPercent) {
    return this._zoom;
};

ShapeManager.prototype.setStrokeColor = function setStrokeColor(strokeColor) {
    this._strokeColor = strokeColor;
    var selected = this.getSelectedShapes();
    for (var s=0; s<selected.length; s++) {
        selected[s].setStrokeColor(strokeColor);
    }
};

ShapeManager.prototype.getStrokeColor = function getStrokeColor() {
    return this._strokeColor;
};

ShapeManager.prototype.setStrokeWidth = function setStrokeWidth(strokeWidth) {
    strokeWidth = parseFloat(strokeWidth, 10);
    this._strokeWidth = strokeWidth;
    var selected = this.getSelectedShapes();
    for (var s=0; s<selected.length; s++) {
        selected[s].setStrokeWidth(strokeWidth);
    }
};

ShapeManager.prototype.getStrokeWidth = function getStrokeWidth() {
    return this._strokeWidth;
};

ShapeManager.prototype.getShapesJson = function getShapesJson() {
    var data = [];
    this.getShapes().forEach(function(s){
        data.push(s.toJson());
    });
    return data;
};

ShapeManager.prototype.setShapesJson = function setShapesJson(jsonShapes) {
    this.deleteAllShapes();
    var self = this;
    jsonShapes.forEach(function(s){
        self.addShapeJson(s);
    });
};

ShapeManager.prototype.regionToPath = function regionToPath(region, zoom) {
    var f = zoom ? zoom/100: this._zoom/100,
        x = parseInt(region.x * f, 10),
        y = parseInt(region.y * f, 10),
        width = parseInt(region.width * f, 10),
        height = parseInt(region.height * f, 10);

    return [["M" + x + "," + y],
                ["L" + (x + width) + "," + y],
                ["L" + (x + width) + "," + (y + height)],
                ["L" + x + "," + (y + height) + "Z"]
            ].join(",");
};

ShapeManager.prototype.findShapeAtCoords = function findShapeAtCoords(jsonShape) {

    var thisShapes = this.getShapes();
    for (var i=0; i<thisShapes.length; i++){
        if (thisShapes[i].compareCoords(jsonShape)) {
            return thisShapes[i];
        }
    }
    return false;
};

// Add new shapes from json but, IF it matches existing shape - offset a bit
ShapeManager.prototype.pasteShapesJson = function pasteShapesJson(jsonShapes, constrainRegion) {
    var self = this,
        newShapes = [],
        allPasted = true;
    // For each shape we want to paste...
    jsonShapes.forEach(function(s){
        // Create a shape to resolve any transform matrix -> coords
        var temp = self.createShapeJson(s);
        s = temp.toJson();
        temp.destroy();
        // check if a shape is at the same coordinates...
        var match = self.findShapeAtCoords(s);
        // if so, keep offsetting until we find a spot...
        while(match) {
            s = $.extend({}, s);
            s = match.offsetCoords(s, 20, 10);
            match = self.findShapeAtCoords(s);
        }
        // Create shape and test if it's in the specified region
        var added = self.addShapeJson(s, constrainRegion);
        if (added) {
            newShapes.push(added);
        } else {
            allPasted = false;
        }
    });
    // Select the newly added shapes
    this.selectShapes(newShapes);
    return allPasted;
};

ShapeManager.prototype.addShapesJson = function addShapesJson(jsonShapes, constrainRegion) {
    var allAdded = true;
    jsonShapes.forEach(function(s){
        var added = this.addShapeJson(s, constrainRegion);
        if (!added) {
            allAdded = false;
        }
    }.bind(this));
    return allAdded;
};

// Create and add a json shape object
// Use constrainRegion {x, y, width, height} to enforce if it's in the specified region
// constrainRegion = true will use the whole image plane
// Return false if shape didn't get created
ShapeManager.prototype.addShapeJson = function addShapeJson(jsonShape, constrainRegion) {
    var newShape = this.createShapeJson(jsonShape);
    if (!newShape) {
        return;
    }
    if (constrainRegion) {
        if (typeof constrainRegion === "boolean") {
            constrainRegion = {x: 0, y: 0, width: this._orig_width, height: this._orig_height};
        }
        if (!newShape.intersectRegion(constrainRegion)) {
            newShape.destroy();
            return false;
        }
    }
    this._shapes.push(newShape);
    return newShape;
};

// Create a Shape object from json
ShapeManager.prototype.createShapeJson = function createShapeJson(jsonShape) {
    var s = jsonShape,
        newShape,
        strokeColor = s.strokeColor || this.getStrokeColor(),
        strokeWidth = s.strokeWidth || this.getStrokeWidth(),
        zoom = this.getZoom(),
        options = {'manager': this,
                   'paper': this.paper,
                   'strokeWidth': strokeWidth,
                   'zoom': zoom,
                   'strokeColor': strokeColor};
    if (jsonShape.id) {
        options.id = jsonShape.id;
    }

    if (s.type === 'Ellipse') {
        options.x = s.x;
        options.y = s.y;
        options.radiusX = s.radiusX;
        options.radiusY = s.radiusY;
        options.rotation = s.rotation || 0;
        options.transform = s.transform;
        newShape = new Ellipse(options);
    }
    else if (s.type === 'Rectangle') {
        options.x = s.x;
        options.y = s.y;
        options.width = s.width;
        options.height = s.height;
        newShape = new Rect(options);
    }
    else if (s.type === 'Line') {
        options.x1 = s.x1;
        options.y1 = s.y1;
        options.x2 = s.x2;
        options.y2 = s.y2;
        newShape = new Line(options);
    }
    else if (s.type === 'Arrow') {
        options.x1 = s.x1;
        options.y1 = s.y1;
        options.x2 = s.x2;
        options.y2 = s.y2;
        newShape = new Arrow(options);
    }
    else if (s.type === 'Polygon') {
        options.points = s.points;
        newShape = new Polygon(options);
    } else if (s.type === 'Polyline') {
        options.points = s.points;
        newShape = new Polyline(options);
    }
    return newShape;
};

// Add a shape object
ShapeManager.prototype.addShape = function addShape(shape) {
    this._shapes.push(shape);
    this.$el.trigger("new:shape", [shape]);
};

ShapeManager.prototype.getShapes = function getShapes() {
    return this._shapes;
};

ShapeManager.prototype.getShape = function getShape(shapeId) {
    var shapes = this.getShapes();
    for (var i=0; i<shapes.length; i++) {
        if (shapes[i]._id === shapeId) {
            return shapes[i];
        }
    }
};

ShapeManager.prototype.getSelectedShapes = function getSelectedShapes() {
    var selected = [],
        shapes = this.getShapes();
    for (var i=0; i<shapes.length; i++) {
        if (shapes[i].isSelected()) {
            selected.push(shapes[i]);
        }
    }
    return selected;
};

ShapeManager.prototype.getSelectedShapesJson = function getShapesJson() {
    var data = [];
    this.getShapes().forEach(function(s){
        if (s.isSelected()) {
            data.push(s.toJson());
        }
    });
    return data;
};

ShapeManager.prototype.getShapeBoundingBox = function getShapeBoundingBox(shapeId) {
    var shape = this.getShape(shapeId);
    var bbox = shape.element.getBBox();
    var zoomFraction = this.getZoom() / 100;
    return {
        x: bbox.x / zoomFraction,
        y: bbox.y / zoomFraction,
        width: bbox.width / zoomFraction,
        height: bbox.height / zoomFraction
    };
};

// Shift all selected shapes by x and y
// E.g. while dragging multiple shapes
ShapeManager.prototype.moveSelectedShapes = function moveSelectedShapes(dx, dy, silent) {
    this.getSelectedShapes().forEach(function(shape){
        shape.offsetShape(dx, dy);
    });
};

ShapeManager.prototype.deleteAllShapes = function deleteAllShapes() {
    this.getShapes().forEach(function(s) {
        s.destroy();
    });
    this._shapes = [];
    this.$el.trigger("change:selected");
};

ShapeManager.prototype.deleteShapesByIds = function deleteShapesByIds(shapeIds) {
    var notSelected = [];
    this.getShapes().forEach(function(s) {
        if (shapeIds.indexOf(s._id) > -1) {
            s.destroy();
        } else {
            notSelected.push(s);
        }
    });
    this._shapes = notSelected;
    this.$el.trigger("change:selected");
};

ShapeManager.prototype.deleteSelectedShapes = function deleteSelectedShapes() {
    var notSelected = [];
    this.getShapes().forEach(function(s) {
        if (s.isSelected()) {
            s.destroy();
        } else {
            notSelected.push(s);
        }
    });
    this._shapes = notSelected;
    this.$el.trigger("change:selected");
};

ShapeManager.prototype.selectShapesById = function selectShapesById(shapeId) {

    // Clear selected with silent:true, since we notify again below
    this.clearSelectedShapes(true);
    var toSelect = [];
    this.getShapes().forEach(function(shape){
        if (shape.toJson().id === shapeId) {
            toSelect.push(shape);
        }
    });
    this.selectShapes(toSelect);
};

ShapeManager.prototype.clearSelectedShapes = function clearSelectedShapes(silent) {
    for (var i=0; i<this._shapes.length; i++) {
        this._shapes[i].setSelected(false);
    }
    if (!silent) {
        this.$el.trigger("change:selected");
    }
};

ShapeManager.prototype.selectShapesByRegion = function selectShapesByRegion(region) {

    // Clear selected with silent:true, since we notify again below
    this.clearSelectedShapes(true);

    var toSelect = [];
    this.getShapes().forEach(function(shape){
        if (shape.intersectRegion(region)) {
            toSelect.push(shape);
        }
    });
    this.selectShapes(toSelect);
};

ShapeManager.prototype.selectAllShapes = function selectAllShapes(region) {
    this.selectShapes(this.getShapes());
};

// select shapes: 'shape' can be shape object or ID
ShapeManager.prototype.selectShapes = function selectShapes(shapes) {

    var strokeColor,
        strokeWidth;

    // Clear selected with silent:true, since we notify again below
    this.clearSelectedShapes(true);

    // Each shape, set selected and get color & stroke width...
    shapes.forEach(function(shape, i){
        if (typeof shape === "number") {
            shape = this.getShape(shape);
        }
        if (shape) {
            // for first shape, pick color
            if (strokeColor === undefined) {
                strokeColor = shape.getStrokeColor();
            } else {
                // for subsequent shapes, if colors don't match - set false
                if (strokeColor !== shape.getStrokeColor()) {
                    strokeColor = false;
                }
            }
            // for first shape, pick strokeWidth
            if (strokeWidth === undefined) {
                strokeWidth = shape.getStrokeWidth();
            } else {
                // for subsequent shapes, if colors don't match - set false
                if (strokeWidth !== shape.getStrokeWidth()) {
                    strokeWidth = false;
                }
            }
            shape.setSelected(true);
        }
    });
    if (strokeColor) {
        this._strokeColor = strokeColor;
    }
    if (strokeWidth) {
        this._strokeWidth = strokeWidth;
    }
    this.$el.trigger("change:selected");
};

ShapeManager.prototype.notifySelectedShapesChanged = function notifySelectedShapesChanged() {
    this.notifyShapesChanged(this.getSelectedShapes());
};

ShapeManager.prototype.notifyShapesChanged = function notifyShapesChanged(shapes) {
    this.$el.trigger("change:shape", [shapes]);
};

ShapeManager.prototype.getRandomId = function getRandomId() {
    // returns a random integer we can use for id
    // NB - we use negative numbers to distinguish from server-side IDs
    var rndString = Math.random() + "";     // E.g. 0.7158358106389642
    return -parseInt(rndString.slice(2), 10);    // -7158358106389642
};
