/*
* Copyright (C) 1998-2018 by Northwoods Software Corporation
* All Rights Reserved.
*
* Floorplan Class
* A Floorplan is a Diagram with special rules
*/

import * as go from "../../../release/go"
import WallBuildingTool = require("./WallBuildingTool")
import WallReshapingTool = require("./WallReshapingTool")

class Floorplan extends go.Diagram {	

	private _palettes: Array<go.Palette>;
	private _pointNodes: go.Set<go.Node>;
	private _dimensionLinks: go.Set<go.Link>;
	private _angleNodes: go.Set<go.Node>;

	constructor(div) {
		super(div);

		/*
	    * Floor Plan Setup:
	    * Initialize Floor Plan, Floor Plan Listeners, Floor Plan Overview
	    */

	    // When a FloorplanPalette instance is made, it is automatically added to a Floorplan's "palettes" property
	    this._palettes = [];

	    // Point Nodes, Dimension Links, Angle Nodes on the Floorplan (never in model data)
	    this._pointNodes = new go.Set(/*go.Node*/);
	    this._dimensionLinks = new go.Set(/*go.Link*/);
	    this._angleNodes = new go.Set(/*go.Node*/);

	    var $ = go.GraphObject.make;

	    this.allowLink = false;
	    this.undoManager.isEnabled = true;
	    this.layout.isOngoing = false;
	    this.model = $(go.GraphLinksModel, {
	        modelData: {
	            "units": "centimeters",
	            "unitsAbbreviation": "cm",
	            "gridSize": 10,
	            "wallThickness": 5,
	            "preferences": {
	                showWallGuidelines: true,
	                showWallLengths: true,
	                showWallAngles: true,
	                showOnlySmallWallAngles: true,
	                showGrid: true,
	                gridSnap: true
	            }
	        }
	    });

	    this.grid = $(go.Panel, "Grid",
	        { gridCellSize: new go.Size(this.model.modelData.gridSize, this.model.modelData.gridSize), visible: true },
	        $(go.Shape, "LineH", { stroke: "lightgray" }),
	        $(go.Shape, "LineV", { stroke: "lightgray" }));
	    this.contextMenu = makeContextMenu();
	    this.commandHandler.canGroupSelection = function () { return true; };
	    this.commandHandler.canUngroupSelection = function () { return true; };
	    this.commandHandler.archetypeGroupData = { isGroup: true };

	    /*
	    * Listeners
	    */

	    // if a wall is copied, update its geometry
	    this.addDiagramListener("SelectionCopied", function (e) {
	    	let fp: Floorplan = <Floorplan> e.diagram;
	        fp.selection.iterator.each(function(part){
	            if (part.category == "WallGroup") {
	            	var w: go.Group = <go.Group>part;
	                fp.updateWall(w);
	            }
	        });
	    });

	    // If a node has been dropped onto the Floorplan from a Palette...
	    this.addDiagramListener("ExternalObjectsDropped", function (e) {
	        var garbage = [];
	        let fp: Floorplan = <Floorplan> e.diagram;
	        fp.selection.iterator.each(function(node){
	            // Event 1: handle a drag / drop of a wall node from the Palette (as opposed to wall construction via WallBuildingTool)
	            if (node.category === "PaletteWallNode") {
	                var paletteWallNode = node;
	                var endpoints = getWallPartEndpoints(paletteWallNode);
	                var data = { key: "wall", category: "WallGroup", caption: "Wall", startpoint: endpoints[0], endpoint: endpoints[1], thickness: parseFloat(e.diagram.model.modelData.wallThickness), isGroup: true, notes: "" };
	                e.diagram.model.addNodeData(data);
	                let wall: go.Group = <go.Group>e.diagram.findPartForKey(data.key);
	                fp.updateWall(wall);
	                garbage.push(paletteWallNode);
	            }
	        });
	        for (var i in garbage) {
	            e.diagram.remove(garbage[i]);
	        }
	    });

	    // When a wall is copied / pasted, update the wall geometry, angle, etc
	    this.addDiagramListener("ClipboardPasted", function (e) {
	    	let fp: Floorplan = <Floorplan> e.diagram;
	        e.diagram.selection.iterator.each(function (node) { 
	        	if (node.category === "WallGroup") {
	        		let w: go.Group = <go.Group>node;
	        		fp.updateWall(w);
        		}
        	});
	    });

		/*
	    * Node Templates
	    * Add Default Node, Multi-Purpose Node, Window Node, Palette Wall Node, and Door Node to the Node Template Map
	    */

	    this.nodeTemplateMap.add("", makeDefaultNode()); // Default Node (furniture)
	    this.nodeTemplateMap.add("MultiPurposeNode", makeMultiPurposeNode()); // Multi-Purpose Node
	    this.nodeTemplateMap.add("WindowNode", makeWindowNode()); // Window Node
	    this.nodeTemplateMap.add("PaletteWallNode", makePaletteWallNode()); // Palette Wall Node
	    this.nodeTemplateMap.add("DoorNode", makeDoorNode()); // Door Node

	    /*
	    * Group Templates
	    * Add Default Group, Wall Group to Group Template Map
	    */

	    this.groupTemplateMap.add("", makeDefaultGroup()); // Default Group
	    this.groupTemplateMap.add("WallGroup", makeWallGroup()); // Wall Group

	    /*
	    * Install Custom Tools
	    * Wall Building Tool, Wall Reshaping Tool
	    */

	    var wallBuildingTool = new WallBuildingTool();
	    this.toolManager.mouseDownTools.insertAt(0, wallBuildingTool);

	    var wallReshapingTool = new WallReshapingTool();
	    this.toolManager.mouseDownTools.insertAt(3, wallReshapingTool);
	    wallBuildingTool.isEnabled = false;

	    /*
	    * Tool Overrides
	    */

	    // If a wall was dragged to intersect another wall, update angle displays
	    this.toolManager.draggingTool.doMouseUp = function () {
	        go.DraggingTool.prototype.doMouseUp.call(this);
	        this.diagram.updateWallAngles();
	        this.isGridSnapEnabled = this.diagram.model.modelData.preferences.gridSnap;
	    }

	    // If user holds SHIFT while dragging, do not use grid snap
	    this.toolManager.draggingTool.doMouseMove = function () {
	        if (this.diagram.lastInput.shift) {
	            this.isGridSnapEnabled = false;
	        } else this.isGridSnapEnabled = this.diagram.model.modelData.preferences.gridSnap;
	        go.DraggingTool.prototype.doMouseMove.call(this);
	    }

	    // When resizing, constantly update the node info box with updated size info; constantly update Dimension Links
	    this.toolManager.resizingTool.doMouseMove = function () {
	        var floorplan = this.diagram;
	        var node = this.adornedObject;
	        this.diagram.updateWallDimensions();
	        go.ResizingTool.prototype.doMouseMove.call(this);
	    }

	    // When resizing a wallPart, do not allow it to be resized past the nearest wallPart / wall endpoints
	    this.toolManager.resizingTool.computeMaxSize = function () {
	        var tool = this;
	        var obj = tool.adornedObject.part;
	        var wall = this.diagram.findPartForKey(obj.data.group);
	        if ((obj.category === 'DoorNode' || obj.category === 'WindowNode') && wall !== null) {
	            var stationaryPt; var movingPt;
	            var resizeAdornment = null;
	            obj.adornments.iterator.each(function (adorn) { if (adorn.name === "WallPartResizeAdornment") resizeAdornment = adorn; });
	            resizeAdornment.elements.iterator.each(function (el) {
	                if (el instanceof go.Shape && el.alignment === tool.handle.alignment) movingPt = el.getDocumentPoint(go.Spot.Center);
	                if (el instanceof go.Shape && el.alignment !== tool.handle.alignment) stationaryPt = el.getDocumentPoint(go.Spot.Center);
	            });
	            // find the constrainingPt; that is, the endpoint (wallPart endpoint or wall endpoint) that is the one closest to movingPt but still farther from stationaryPt than movingPt
	            // this loop checks all other wallPart endpoints of the wall that the resizing wallPart is a part of
	            var constrainingPt; var closestDist = Number.MAX_VALUE;
	            wall.memberParts.iterator.each(function (part) {
	                if (part.data.key !== obj.data.key) {
	                    var endpoints = getWallPartEndpoints(part);
	                    for (var i = 0; i < endpoints.length; i++) {
	                        var point = endpoints[i];
	                        var distanceToMovingPt = Math.sqrt(point.distanceSquaredPoint(movingPt));
	                        if (distanceToMovingPt < closestDist) {
	                            var distanceToStationaryPt = Math.sqrt(point.distanceSquaredPoint(stationaryPt));
	                            if (distanceToStationaryPt > distanceToMovingPt) {
	                                closestDist = distanceToMovingPt;
	                                constrainingPt = point;
	                            }
	                        }
	                    }
	                }
	            });
	            // if we're not constrained by a wallPart endpoint, the constraint will come from a wall endpoint; figure out which one
	            if (constrainingPt === undefined || constrainingPt === null) {
	                if (wall.data.startpoint.distanceSquaredPoint(movingPt) > wall.data.startpoint.distanceSquaredPoint(stationaryPt)) constrainingPt = wall.data.endpoint;
	                else constrainingPt = wall.data.startpoint;
	            }
	            // set the new max size of the wallPart according to the constrainingPt
	            var maxLength = Math.sqrt(stationaryPt.distanceSquaredPoint(constrainingPt));
	            return new go.Size(maxLength, wall.data.thickness);
	        }
	        return go.ResizingTool.prototype.computeMaxSize.call(tool);
	    }

	    this.toolManager.draggingTool.isGridSnapEnabled = true;

	} // end Floorplan constructor 

	/**
	* Get / set array of all Palettes associated with this Floorplan
	*/
	get palettes(): Array<go.Palette> { return this._palettes }
	set palettes(value: Array<go.Palette>) { this._palettes = value; }

	/**
	* Get / set pointNodes
	*/
	get pointNodes(): go.Set<go.Node> { return this._pointNodes }
	set pointNodes(value: go.Set<go.Node>) { this._pointNodes = value; }

	/**
	* Get / set dimensionLinks
	*/
	get dimensionLinks(): go.Set<go.Link> { return this._dimensionLinks }
	set dimensionLinks(value: go.Set<go.Link>) { this._dimensionLinks = value; }

	/**
	* Get / set angleNodes
	*/
	get angleNodes(): go.Set<go.Node> { return this._angleNodes }
	set angleNodes(value: go.Set<go.Node>) { this._angleNodes = value; }

	// Check what units are being used, convert to cm then multiply by 2, (1px = 2cm, change this if you want to use a different paradigm)
	public convertPixelsToUnits = function (num: number) {
	    let units: string = this.model.modelData.units;
	    let factor: number = this.model.modelData.unitsConversionFactor;
	    if (units === 'meters') return (num / 100) * factor;
	    if (units === 'feet') return (num / 30.48) * factor;
	    if (units === 'inches') return (num / 2.54) * factor;
	    return num * factor;
	}

	// Take a number of units, convert to cm, then divide by 2, (1px = 2cm, change this if you want to use a different paradigm)
	public convertUnitsToPixels = function (num: number) {
	    let units: string = this.model.modelData.units;
	    let factor: number = this.model.modelData.unitsConversionFactor;
	    if (units === 'meters') return (num * 100) / factor;
	    if (units === 'feet') return (num * 30.48) / factor;
	    if (units === 'inches') return (num * 2.54) / factor;
	    return num / factor;
	}

	/*
	* Update the geometry, angle, and location of a given wall
	* @param {Wall} wall A reference to a valid Wall Group (defined in Templates-Walls)
	*/
	public updateWall = function (wall: go.Group) {
	    if (wall.data.startpoint && wall.data.endpoint) {
	        let shape: go.Shape = <go.Shape>wall.findObject("SHAPE");
	        let geo: go.Geometry = new go.Geometry(go.Geometry.Line);
	        let sPt: go.Point = wall.data.startpoint;
	        let ePt: go.Point = wall.data.endpoint;
	        let mPt: go.Point = new go.Point((sPt.x + ePt.x) / 2, (sPt.y + ePt.y) / 2);
	        // define a wall's geometry as a simple horizontal line, then rotate it
	        geo.startX = 0;
	        geo.startY = 0;
	        geo.endX = Math.sqrt(sPt.distanceSquaredPoint(ePt));
	        geo.endY = 0;
	        shape.geometry = geo;
	        wall.location = mPt; // a wall's location is the midpoint between it's startpoint and endpoint
	        let angle: number = sPt.directionPoint(ePt);
	        wall.rotateObject.angle = angle;
	        this.updateWallDimensions();
	    }
	}

	/*
	* Helper function for Build Dimension Link: get a to/from point for a Dimension Link
	* @param {Wall} wall The Wall Group being given a Dimension Link
	* @param {Number} angle The angle of "wall"
	* @param {Number} wallOffset The distance the Dimension Link will be from wall (in pixels)
	*/
	public getAdjustedPoint = function (point: go.Point, wall: go.Group, angle: number, wallOffset: number): go.Point {
	    let oldPoint: go.Point = point.copy();
	    point.offset(0, -(wall.data.thickness * .5) - wallOffset);
	    point.offset(-oldPoint.x, -oldPoint.y).rotate(angle).offset(oldPoint.x, oldPoint.y);
	    return point;
	}

	/*
	* Helper function for Update Wall Dimensions; used to build Dimension Links
	* @param {Wall} wall The wall the Link runs along (either describing the wall itself or some wallPart on "wall")
	* @param {Number} index A number appended to PointNode keys; used for finding PointNodes of Dimension Links later
	* @param {Point} point1 The first point of the wallPart being described by the Link
	* @param {Point} point2 The second point of the wallPart being described by the Link
	* @param {Number} angle The angle of the wallPart
	* @param {Number} wallOffset How far from the wall (in px) the Link should be
	* @param {Boolean} soloWallFlag If this Link is the only Dimension Link for "wall" (no other wallParts on "wall" selected) this is true; else, false
	* @param {Floorplan} floorplan A reference to a valid Floorplan
	*/
	public buildDimensionLink = function (wall: go.Group, index: number, point1: go.Point, point2: go.Point, angle: number, wallOffset: number, soloWallFlag: boolean, floorplan: Floorplan) {
	    point1 = floorplan.getAdjustedPoint(point1, wall, angle, wallOffset);
	    point2 = floorplan.getAdjustedPoint(point2, wall, angle, wallOffset);
	    let data1 = { key: wall.data.key + "PointNode" + index, category: "PointNode", loc: go.Point.stringify(point1) };
	    let data2 = { key: wall.data.key + "PointNode" + (index + 1), category: "PointNode", loc: go.Point.stringify(point2) };
	    let data3 = { key: wall.data.key + "DimensionLink", category: 'DimensionLink', from: data1.key, to: data2.key, stroke: 'gray', angle: angle, wall: wall.data.key, soloWallFlag: soloWallFlag };
	    let pointNode1: go.Node = makePointNode();
	    let pointNode2: go.Node = makePointNode();
	    let link: go.Link = makeDimensionLink();

	    floorplan.pointNodes.add(pointNode1);
	    floorplan.pointNodes.add(pointNode2);
	    floorplan.dimensionLinks.add(link);
	    floorplan.add(pointNode1);
	    floorplan.add(pointNode2);
	    floorplan.add(link);

	    pointNode1.data = data1;
	    pointNode2.data = data2;
	    link.data = data3;
	    link.fromNode = pointNode1;
	    link.toNode = pointNode2;
	}

	/*
	* Update Dimension Links shown along walls, based on which walls and wallParts are selected
	*/
	public updateWallDimensions = function () {
	    const floorplan: Floorplan = this;
	    floorplan.skipsUndoManager = true;
	    floorplan.startTransaction("update wall dimensions");
	    // if showWallLengths === false, remove all pointNodes (used to build wall dimensions)
	    if (!floorplan.model.modelData.preferences.showWallLengths) {
	        floorplan.pointNodes.iterator.each(function (node) { floorplan.remove(node); });
	        floorplan.dimensionLinks.iterator.each(function (link) { floorplan.remove(link); });
	        floorplan.pointNodes.clear();
	        floorplan.dimensionLinks.clear();
	        floorplan.commitTransaction("update wall dimensions");
	        floorplan.skipsUndoManager = false;
	        return;
	    }
	    // make visible all dimension links (zero-length dimension links are set to invisible at the end of the function)
	    floorplan.dimensionLinks.iterator.each(function (link) { link.visible = true; });

	    let selection: go.Set<go.Part> = floorplan.selection;
	    // gather all selected walls, including walls of selected DoorNodes and WindowNodes
	    let walls: go.Set<go.Group> = new go.Set(/*go.Group*/);
	    selection.iterator.each(function (part) {
	        if ((part.category === 'WindowNode' || part.category === 'DoorNode') && part.containingGroup !== null) walls.add(part.containingGroup);
	        if (part.category === 'WallGroup' && part.data && part.data.startpoint && part.data.endpoint) {
	        	let wall: go.Group = <go.Group>part;
	            let soloWallLink = null;
	            floorplan.dimensionLinks.iterator.each(function (link) { if (link.data.soloWallFlag && link.data.wall === wall.data.key) soloWallLink = link; });
	            // if there's 1 Dimension Link for this wall (link has soloWallFlag), adjust to/from pointNodes of link, rather than deleting / redrawing
	            if (soloWallLink !== null) {
	                // since this is the only Dimension Link for this wall, keys of its pointNodes will be (wall.data.key) + 1 / (wall.data.key) + 2
	                let linkPoint1: go.Node = null; let linkPoint2: go.Node = null;
	                floorplan.pointNodes.iterator.each(function (node) {
	                    if (node.data.key === wall.data.key + "PointNode1") linkPoint1 = node;
	                    if (node.data.key === wall.data.key + "PointNode2") linkPoint2 = node;
	                });
	                let startpoint: go.Point = wall.data.startpoint; let endpoint: go.Point = wall.data.endpoint;
	                // adjust  left/top-most / right/bottom-most wall endpoints so link angle is correct (else text appears on wrong side of Link)
	                let firstWallPt: go.Point = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
	                let lastWallPt: go.Point = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
	                let newLoc1: go.Point = floorplan.getAdjustedPoint(firstWallPt.copy(), wall, wall.rotateObject.angle, 10);
	                let newLoc2: go.Point = floorplan.getAdjustedPoint(lastWallPt.copy(), wall, wall.rotateObject.angle, 10);
	                // cannot use model.setDataProperty, since pointNodes and dimensionLinks are not stored in the model
	                linkPoint1.data.loc = go.Point.stringify(newLoc1);
	                linkPoint2.data.loc = go.Point.stringify(newLoc2);
	                soloWallLink.data.angle = wall.rotateObject.angle;
	                linkPoint1.updateTargetBindings();
	                linkPoint2.updateTargetBindings();
	                soloWallLink.updateTargetBindings();
	            }
	            // else build a Dimension Link for this wall; this is removed / replaced if Dimension Links for wallParts this wall are built
	            else {
	                let startpoint: go.Point = wall.data.startpoint;
	                let endpoint: go.Point = wall.data.endpoint;
	                let firstWallPt: go.Point = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
	                let lastWallPt: go.Point = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
	                floorplan.buildDimensionLink(wall, 1, firstWallPt.copy(), lastWallPt.copy(), wall.rotateObject.angle, 10, true, floorplan);
	            }
	        }
	    });
	    // create array of selected wall endpoints and selected wallPart endpoints along the wall that represent measured stretches
	    walls.iterator.each(function (wall) {
	        let startpoint: go.Point = wall.data.startpoint;
	        let endpoint: go.Point = wall.data.endpoint;
	        let firstWallPt: go.Point = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
	        let lastWallPt: go.Point = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;

	        // store all endpoints along with the part they correspond to (used later to either create DimensionLinks or simply adjust them)
	        let wallPartEndpoints: Array<go.Point> = [];
	        wall.memberParts.iterator.each(function (wallPart) {
	            if (wallPart.isSelected) {
	                var endpoints = getWallPartEndpoints(wallPart);
	                wallPartEndpoints.push(endpoints[0]);
	                wallPartEndpoints.push(endpoints[1]);
	            }
	        });
	        // sort all wallPartEndpoints by x coordinate left to right/ up to down
	        wallPartEndpoints.sort(function (a, b) {
	            if ((a.x + a.y) > (b.x + b.y)) return 1;
	            if ((a.x + a.y) < (b.x + b.y)) return -1;
	            else return 0;
	        });
	        wallPartEndpoints.unshift(firstWallPt);
	        wallPartEndpoints.push(lastWallPt);

	        let angle: number = wall.rotateObject.angle;
	        let k: number = 1; // k is a counter for the indices of PointNodes
	        // build / edit dimension links for each stretch, defined by pairs of points in wallPartEndpoints
	        for (let j: number = 0; j < wallPartEndpoints.length - 1; j++) {
	            let linkPoint1: go.Node = null; let linkPoint2: go.Node = null;
	            floorplan.pointNodes.iterator.each(function (node) {
	                if (node.data.key === wall.data.key + "PointNode" + k) linkPoint1 = node;
	                if (node.data.key === wall.data.key + "PointNode" + (k + 1)) linkPoint2 = node;
	            });
	            if (linkPoint1 !== null) {
	                let newLoc1: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[j].copy(), wall, angle, 5);
	                let newLoc2: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[j + 1].copy(), wall, angle, 5);
	                linkPoint1.data.loc = go.Point.stringify(newLoc1);
	                linkPoint2.data.loc = go.Point.stringify(newLoc2);
	                linkPoint1.updateTargetBindings();
	                linkPoint2.updateTargetBindings();
	            }
	            // only build new links if needed -- normally simply change pointNode locations
	            else floorplan.buildDimensionLink(wall, k, wallPartEndpoints[j].copy(), wallPartEndpoints[j + 1].copy(), angle, 5, false, floorplan);
	            k += 2;
	        }
	        // total wall Dimension Link constructed of a kth and k+1st pointNode
	        let totalWallDimensionLink = null;
	        floorplan.dimensionLinks.iterator.each(function (link) {
	            if ((link.fromNode.data.key === wall.data.key + "PointNode" + k) &&
	                (link.toNode.data.key === wall.data.key + "PointNode" + (k + 1))) totalWallDimensionLink = link;
	        });
	        // if a total wall Dimension Link already exists, adjust its constituent point nodes
	        if (totalWallDimensionLink !== null) {
	            let linkPoint1: go.Node = null; let linkPoint2: go.Node = null;
	            floorplan.pointNodes.iterator.each(function (node) {
	                if (node.data.key === wall.data.key + "PointNode" + k) linkPoint1 = node;
	                if (node.data.key === wall.data.key + "PointNode" + (k + 1)) linkPoint2 = node;
	            });
	            let newLoc1: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[0].copy(), wall, angle, 25);
	            let newLoc2: go.Point = floorplan.getAdjustedPoint(wallPartEndpoints[wallPartEndpoints.length - 1].copy(), wall, angle, 25);
	            linkPoint1.data.loc = go.Point.stringify(newLoc1);
	            linkPoint2.data.loc = go.Point.stringify(newLoc2);
	            linkPoint1.updateTargetBindings();
	            linkPoint2.updateTargetBindings();
	        }
	            // only build total wall Dimension Link (far out from wall to accomodate wallPart Dimension Links) if one does not already exist
	        else floorplan.buildDimensionLink(wall, k, wallPartEndpoints[0].copy(), wallPartEndpoints[wallPartEndpoints.length - 1].copy(), angle, 25, false, floorplan);
	    });

	    // Cleanup: hide zero-length Dimension Links, DimensionLinks with null wall points
	    floorplan.dimensionLinks.iterator.each(function (link) {
	        let canStay: boolean = false;
	        floorplan.pointNodes.iterator.each(function (node) {
	            if (node.data.key == link.data.to) canStay = true;
	        });
	        if (!canStay) floorplan.remove(link);
	        else {
	            let length: number = Math.sqrt(link.toNode.location.distanceSquaredPoint(link.fromNode.location));
	            if (length < 1 && !link.data.soloWallFlag) link.visible = false;
	        }
	    });

	    floorplan.commitTransaction("update wall dimensions");
	    floorplan.skipsUndoManager = false;
	} // end updateWallDimensions()

	/*
	* Helper function for updateWallAngles(); returns the Point where two walls intersect; if they do not intersect, return null
	* @param {Wall} wall1
	* @param {Wall} wall2
	*/
	public getWallsIntersection = function (wall1: go.Group, wall2: go.Group): go.Point {
	    if (wall1 === null || wall2 === null) return null;
	    // treat walls as lines; get lines in formula of ax + by = c
	    let a1: number = wall1.data.endpoint.y - wall1.data.startpoint.y;
	    let b1: number = wall1.data.startpoint.x - wall1.data.endpoint.x;
	    let c1: number = (a1 * wall1.data.startpoint.x) + (b1 * wall1.data.startpoint.y);
	    let a2: number = wall2.data.endpoint.y - wall2.data.startpoint.y;
	    let b2: number = wall2.data.startpoint.x - wall2.data.endpoint.x;
	    let c2: number = (a2 * wall2.data.startpoint.x) + (b2 * wall2.data.startpoint.y);
	    // Solve the system of equations, finding where the lines (not segments) would intersect
	    /** Algebra Explanation:
	        Line 1: a1x + b1y = c1
	        Line 2: a2x + b2y = c2

	        Multiply Line1 equation by b2, Line2 equation by b1, get:
	        a1b1x + b1b2y = b2c1
	        a2b1x + b1b2y = b1c2

	        Subtract bottom from top:
	        a1b2x - a2b1x = b2c1 - b1c2

	        Divide both sides by a1b2 - a2b1, get equation for x. Equation for y is analogous
	    **/
	    let det: number = a1 * b2 - a2 * b1;
	    let x: number = null; var y: number = null;
	    // Edge Case: Lines are paralell
	    if (det === 0) {
	        // Edge Case: wall1 and wall2 have an endpoint to endpoint intersection (the only instance in which paralell walls could intersect at a specific point)
	        if (wall1.data.startpoint.equals(wall2.data.startpoint) || wall1.data.startpoint.equals(wall2.data.endpoint)) return wall1.data.startpoint;
	        if (wall1.data.endpoint.equals(wall2.data.startpoint) || wall1.data.endpoint.equals(wall2.data.endpoint)) return wall1.data.endpoint;
	        return null;
	    }
	    else {
	        x = (b2 * c1 - b1 * c2) / det;
	        y = (a1 * c2 - a2 * c1) / det;
	    }
	    // ensure proposed intersection is contained in both line segments (walls)
	    let inWall1: boolean = ((Math.min(wall1.data.startpoint.x, wall1.data.endpoint.x) <= x) && (Math.max(wall1.data.startpoint.x, wall1.data.endpoint.x) >= x)
	        && (Math.min(wall1.data.startpoint.y, wall1.data.endpoint.y) <= y) && (Math.max(wall1.data.startpoint.y, wall1.data.endpoint.y) >= y));
	    let inWall2: boolean = ((Math.min(wall2.data.startpoint.x, wall2.data.endpoint.x) <= x) && (Math.max(wall2.data.startpoint.x, wall2.data.endpoint.x) >= x)
	        && (Math.min(wall2.data.startpoint.y, wall2.data.endpoint.y) <= y) && (Math.max(wall2.data.startpoint.y, wall2.data.endpoint.y) >= y));
	    if (inWall1 && inWall2) return new go.Point(x, y);
	    else return null;
	}

	/*
	* Update Angle Nodes shown along a wall, based on which wall(s) is/are selected
	*/
	public updateWallAngles = function () {
	    const floorplan: Floorplan = this;
	    floorplan.skipsUndoManager = true; // do not store displaying angles as a transaction
	    floorplan.startTransaction("display angles");
	    if (floorplan.model.modelData.preferences.showWallAngles) {
	        floorplan.angleNodes.iterator.each(function (node) { node.visible = true; });
	        let selectedWalls: Array<go.Group> = [];
	        floorplan.selection.iterator.each(function (part) { 
	        	if (part.category === "WallGroup") {
	        		let w: go.Group = <go.Group>part;
	        		selectedWalls.push(w);
        		}
	        });
	        for (let i: number = 0; i < selectedWalls.length; i++) {
	            let seen: go.Set<string> = new go.Set(/*"string"*/); // Set of all walls "seen" thus far for "wall"
	            let wall: go.Group = selectedWalls[i];
	            let possibleWalls: go.Iterator<go.Group> = <go.Iterator<go.Group>>floorplan.findNodesByExample({ category: "WallGroup" });

	            // go through all other walls; if the other wall intersects this wall, make angles
	            possibleWalls.iterator.each(function (otherWall) {
	                if (otherWall.data === null || wall.data === null || seen.contains(otherWall.data.key)) return;
	                if ((otherWall.data.key !== wall.data.key) && (floorplan.getWallsIntersection(wall, otherWall) !== null) && (!seen.contains(otherWall.data.key))) {

	                    seen.add(otherWall.data.key);
	                    // "otherWall" intersects "wall"; make or update angle nodes
	                    let intersectionPoint: go.Point = floorplan.getWallsIntersection(wall, otherWall);
	                    let wallsInvolved: go.Set<go.Group> = <go.Set<go.Group>>floorplan.findObjectsNear(intersectionPoint,
	                        1,
	                        function (x) { if (x.part !== null) return x.part; },
	                        function (p) { 
	                        	if (!(p instanceof go.Group)) return false;
	                        	else return p.category === "WallGroup"; 
	                        },
	                        false);

	                    let endpoints: Array<any> = []; // store endpoints and their corresponding walls here
	                    // gather endpoints of each wall in wallsInvolved; discard endpoints within a tolerance distance of intersectionPoint
	                    wallsInvolved.iterator.each(function (w) {
	                        let tolerance: number = (floorplan.model.modelData.gridSize >= 10) ? floorplan.model.modelData.gridSize : 10;
	                        if (Math.sqrt(w.data.startpoint.distanceSquaredPoint(intersectionPoint)) > tolerance) endpoints.push({ point: w.data.startpoint, wall: w.data.key });
	                        if (Math.sqrt(w.data.endpoint.distanceSquaredPoint(intersectionPoint)) > tolerance) endpoints.push({ point: w.data.endpoint, wall: w.data.key });
	                    });

	                    // find maxRadius (shortest distance from an involved wall's endpoint to intersectionPoint or 30, whichever is smaller)
	                    let maxRadius: number = 30;
	                    for (let i: number = 0; i < endpoints.length; i++) {
	                        let distance: number = Math.sqrt(endpoints[i].point.distanceSquaredPoint(intersectionPoint));
	                        if (distance < maxRadius) maxRadius = distance;
	                    }

	                    // sort endpoints in a clockwise fashion around the intersectionPoint
	                    endpoints.sort(function (a, b) {
	                        a = a.point; b = b.point;
	                        if (a.x - intersectionPoint.x >= 0 && b.x - intersectionPoint.x < 0) return 1;
	                        if (a.x - intersectionPoint.x < 0 && b.x - intersectionPoint.x >= 0) return -1;
	                        if (a.x - intersectionPoint.x == 0 && b.x - intersectionPoint.x == 0) {
	                            if (a.y - intersectionPoint.y >= 0 || b.y - intersectionPoint.y >= 0) return a.y > b.y ? 1 : -1;
	                            return b.y > a.y ? 1 : -1;
	                        }

	                        // compute the cross product of vectors (center -> a) x (center -> b)
	                        let det: number = (a.x - intersectionPoint.x) * (b.y - intersectionPoint.y) - (b.x - intersectionPoint.x) * (a.y - intersectionPoint.y);
	                        if (det < 0) return 1;
	                        if (det > 0) return -1;

	                        // points a and b are on the same line from the center; check which point is closer to the center
	                        let d1: number = (a.x - intersectionPoint.x) * (a.x - intersectionPoint.x) + (a.y - intersectionPoint.y) * (a.y - intersectionPoint.y);
	                        let d2: number = (b.x - intersectionPoint.x) * (b.x - intersectionPoint.x) + (b.y - intersectionPoint.y) * (b.y - intersectionPoint.y);
	                        return d1 > d2 ? 1 : -1;
	                    }); // end endpoints sort

	                    // for each pair of endpoints, construct or modify an angleNode
	                    for (let i: number = 0; i < endpoints.length; i++) {
	                        let p1: any = endpoints[i]; let p2: any;
	                        if (endpoints[i + 1] != null) {
	                        	p2 = endpoints[i + 1];
	                        }
	                        else {
	                        	p2 = endpoints[0];
	                        }
	                        let a1: number = intersectionPoint.directionPoint(p1.point);
	                        let a2: number = intersectionPoint.directionPoint(p2.point);
	                        let sweep: number = Math.abs(a2 - a1 + 360) % 360;
	                        let angle: number = a1;

	                        /*
	                            construct proper key for angleNode
	                            proper angleNode key syntax is "wallWwallX...wallYangleNodeZ" such that W < Y < Y; angleNodes are sorted clockwise around the intersectionPoint by Z
	                        */
	                        let keyArray: Array<go.Group> = []; // used to construct proper key
	                        wallsInvolved.iterator.each(function (wall) { keyArray.push(wall); });
	                        keyArray.sort(function (a, b) {
	                            let aIndex = a.data.key.match(/\d+/g);
	                            let bIndex = b.data.key.match(/\d+/g);
	                            if (isNaN(aIndex)) return 1;
	                            if (isNaN(bIndex)) return -1;
	                            else return aIndex > bIndex ? 1 : -1;
	                        });

	                        let key: string = "";
	                        for (let j: number = 0; j < keyArray.length; j++) key += keyArray[j].data.key;
	                        key += "angle" + i;

	                        // check if this angleNode already exists -- if it does, adjust data (instead of deleting/redrawing)
	                        let angleNode: go.Node = null;
	                        floorplan.angleNodes.iterator.each(function (aNode) { if (aNode.data.key === key) angleNode = aNode; });
	                        if (angleNode !== null) {
	                            angleNode.data.angle = angle;
	                            angleNode.data.sweep = sweep;
	                            angleNode.data.loc = go.Point.stringify(intersectionPoint);
	                            angleNode.data.maxRadius = maxRadius;
	                            angleNode.updateTargetBindings();
	                        }
	                            // if this angleNode does not already exist, create it and add it to the diagram
	                        else {
	                            let data = { key: key, category: "AngleNode", loc: go.Point.stringify(intersectionPoint), stroke: "dodgerblue", angle: angle, sweep: sweep, maxRadius: maxRadius };
	                            let newAngleNode: go.Node = makeAngleNode();
	                            newAngleNode.data = data;
	                            floorplan.add(newAngleNode);
	                            newAngleNode.updateTargetBindings();
	                            floorplan.angleNodes.add(newAngleNode);
	                        }
	                    }
	                }
	            });
	        }
	        // garbage collection (angleNodes that should not exist any more)
	        let garbage: Array<go.Node> = [];
	        floorplan.angleNodes.iterator.each(function (node) {
	            let keyNums = node.data.key.match(/\d+/g); // values X for all wall keys involved, given key "wallX"
	            let numWalls: number = (node.data.key.match(/wall/g) || []).length; // # of walls involved in in "node"'s construction
	            let wallsInvolved: Array<string> = [];
	            // add all walls involved in angleNode's construction to wallsInvolved
	            for (let i: number = 0; i < keyNums.length - 1; i++) wallsInvolved.push("wall" + keyNums[i]);
	            // edge case: if the numWalls != keyNums.length, that means the wall with key "wall" (no number in key) is involved
	            if (numWalls !== keyNums.length - 1) wallsInvolved.push("wall");

	            // Case 1: if any wall pairs involved in this angleNode are no longer intersecting, add this angleNode to "garbage"
	            for (let i: number = 0; i < wallsInvolved.length - 1; i++) {
	                let wall1: go.Group = <go.Group>floorplan.findPartForKey(wallsInvolved[i]);
	                let wall2: go.Group = <go.Group>floorplan.findPartForKey(wallsInvolved[i + 1]);
	                let intersectionPoint: go.Point = floorplan.getWallsIntersection(wall1, wall2);
	                if (intersectionPoint === null) garbage.push(node);
	            }
	            // Case 2: if there are angleNode clusters with the same walls in their keys as "node" but different locations, destroy and rebuild
	            // collect all angleNodes with same walls in their construction as "node"
	            let possibleAngleNodes: go.Set<go.Node> = new go.Set(/*go.Node*/);
	            let allWalls = node.data.key.slice(0, node.data.key.indexOf("angle"));
	            floorplan.angleNodes.iterator.each(function (other) { if (other.data.key.indexOf(allWalls) !== -1) possibleAngleNodes.add(other); });
	            possibleAngleNodes.iterator.each(function (pNode) {
	                if (pNode.data.loc !== node.data.loc) {
	                    garbage.push(pNode);
	                }
	            });

	            // Case 3: put any angleNodes with sweep === 0 in garbage
	            if (node.data.sweep === 0) garbage.push(node);
	        });

	        for (let i: number = 0; i < garbage.length; i++) {
	            floorplan.remove(garbage[i]); // remove garbage
	            floorplan.angleNodes.remove(garbage[i]);
	        }
	    }
	    // hide all angles > 180 if show only small angles == true in preferences
	    if (floorplan.model.modelData.preferences.showOnlySmallWallAngles) {
	        floorplan.angleNodes.iterator.each(function (node) { if (node.data.sweep >= 180) node.visible = false; });
	    }
	    // hide all angles if show wall angles == false in preferences
	    if (!floorplan.model.modelData.preferences.showWallAngles) {
	        floorplan.angleNodes.iterator.each(function (node) { node.visible = false; });
	    }
	    floorplan.commitTransaction("display angles");
	    floorplan.skipsUndoManager = false;
	}

}

/*
* Copyright (C) 1998-2018 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - GENERAL
* General GraphObject templates used in the Floor Planner sample   
* Includes Context Menu, Diagram, Default Group, AngleNode, DimensionLink, PointNode 
*/

/*
* Dependencies for Context Menu:
* Make Selection Group, Ungroup Selection, Clear Empty Groups 
*/

// Make the selection a group
function makeSelectionGroup(floorplan) {
    floorplan.startTransaction("group selection");
    // ungroup all selected nodes; then group them; if one of the selected nodes is a group, ungroup all its nodes
    var sel = floorplan.selection; var nodes = [];
    sel.iterator.each(function (n) {
        if (n instanceof go.Group) n.memberParts.iterator.each(function (part) { nodes.push(part); })
        else nodes.push(n);
    });
    for (var i = 0; i < nodes.length; i++) nodes[i].isSelected = true;
    ungroupSelection(floorplan);
    floorplan.commandHandler.groupSelection();
    var group = floorplan.selection.first(); // after grouping, the new group will be the only thing selected
    floorplan.model.setDataProperty(group.data, "caption", "Group");
    floorplan.model.setDataProperty(group.data, "notes", "");
    clearEmptyGroups(floorplan);
    // unselect / reselect group so data appears properly in Selection Info Window
    floorplan.clearSelection();
    floorplan.select(group);
    floorplan.commitTransaction("group selection");
}

// Ungroup selected nodes; if the selection is a group, ungroup all it's memberParts
function ungroupSelection(floorplan) {
    floorplan.startTransaction('ungroup selection');
    // helper function to ungroup nodes
    function ungroupNode(node) {
        var group = node.containingGroup;
        node.containingGroup = null;
        if (group != null) {
            if (group.memberParts.count === 0) floorplan.remove(group);
            else if (group.memberParts.count === 1) group.memberParts.first().containingGroup = null;
        }
    }
    // ungroup any selected nodes; remember groups that are selected
    var sel = floorplan.selection; var groups = [];
    sel.iterator.each(function (n) {
        if (!(n instanceof go.Group)) ungroupNode(n);
        else groups.push(n);
    });
    // go through selected groups, and ungroup their memberparts too
    var nodes = [];
    for (var i = 0; i < groups.length; i++) groups[i].memberParts.iterator.each(function (n) { nodes.push(n); });
    for (var i = 0; i < nodes.length; i++) ungroupNode(nodes[i]);
    clearEmptyGroups(floorplan);
    floorplan.commitTransaction('ungroup selection');
}

// Clear all the groups that have no nodes
function clearEmptyGroups(floorplan) {
    var nodes = floorplan.nodes; var arr = [];
    nodes.iterator.each(function (node) { if (node instanceof go.Group && node.memberParts.count === 0 && node.category !== "WallGroup") { arr.push(node); } });
    for (let i: number = 0; i < arr.length; i++) { floorplan.remove(arr[i]); }
}

/*
* General Group Dependencies:
* Group Tool Tip
*/

// Group Tool Tip
function makeGroupToolTip() {
    var $ = go.GraphObject.make;
    return $(go.Adornment, "Auto",
            $(go.Shape, { fill: "#FFFFCC" }),
            $(go.TextBlock, { margin: 4 },
            new go.Binding("text", "", function (text, obj) {
                var data = obj.part.adornedObject.data;
                var name = (obj.part.adornedObject.category === "MultiPurposeNode") ? data.text : data.caption;
                return "Name: " + name + "\nNotes: " + data.notes + '\nMembers: ' + obj.part.adornedObject.memberParts.count;
            }).ofObject())
        );
}

/*
* General Templates:
* Context Menu, Default Group
*/

// Context Menu -- referenced by Node, Diagram and Group Templates
function makeContextMenu() {
    var $ = go.GraphObject.make
    return $(go.Adornment, "Vertical",
        // Make Selection Group Button
        $("ContextMenuButton",
            $(go.TextBlock, "Make Group"),
            { click: function (e, obj) { makeSelectionGroup(obj.part.diagram); } },
            new go.Binding("visible", "visible", function (v, obj) {
                var floorplan = obj.part.diagram;
                if (floorplan.selection.count <= 1) return false;
                var flag = true;
                floorplan.selection.iterator.each(function (node) {
                    if (node.category === "WallGroup" || node.category === "WindowNode" || node.category === "DoorNode") flag = false;
                });
                return flag;
            }).ofObject()
        ),
        // Ungroup Selection Button
        $("ContextMenuButton",
            $(go.TextBlock, "Ungroup"),
            { click: function (e, obj) { ungroupSelection(obj.part.diagram); } },
            new go.Binding("visible", "", function (v, obj) {
                var floorplan = obj.part.diagram;
                if (floorplan !== null) {
                    var node = floorplan.selection.first();
                    return ((node instanceof go.Node && node.containingGroup != null && node.containingGroup.category != 'WallGroup') ||
                        (node instanceof go.Group && node.category === ''));
                } return false;
            }).ofObject()
        ),
        // Copy Button
        $("ContextMenuButton",
            $(go.TextBlock, "Copy"),
            { click: function (e, obj) { obj.part.diagram.commandHandler.copySelection() } },
            new go.Binding("visible", "", function (v, obj) {
                if (obj.part.diagram !== null) {
                    return obj.part.diagram.selection.count > 0;
                } return false;
            }).ofObject()
        ),
        // Cut Button
        $("ContextMenuButton",
            $(go.TextBlock, "Cut"),
            { click: function (e, obj) { obj.part.diagram.commandHandler.cutSelection() } },
            new go.Binding("visible", "", function (v, obj) {
                if (obj.part.diagram !== null) {
                    return obj.part.diagram.selection.count > 0;
                } return false;
            }).ofObject()
        ),
        // Delete Button
        $("ContextMenuButton",
            $(go.TextBlock, "Delete"),
            { click: function (e, obj) { obj.part.diagram.commandHandler.deleteSelection() } },
            new go.Binding("visible", "", function (v, obj) {
                if (obj.part.diagram !== null) {
                    return obj.part.diagram.selection.count > 0;
                } return false;
            }).ofObject()
        ),
        // Paste Button
        $("ContextMenuButton",
            $(go.TextBlock, "Paste"),
            { click: function (e, obj) { obj.part.diagram.commandHandler.pasteSelection(obj.part.diagram.lastInput.documentPoint) } }
        ),
        // Show Selection Info Button (only available when selection count > 0)
        $("ContextMenuButton",
            $(go.TextBlock, "Show Selection Info"),
            {
                click: function (e, obj) {
                    if (e.diagram.floorplanUI) {
                        var selectionInfoWindow = document.getElementById(e.diagram.floorplanUI.state.windows.selectionInfoWindow.id);
                        if (selectionInfoWindow.style.visibility !== 'visible') e.diagram.floorplanUI.hideShow('selectionInfoWindow');
                    }
                }
            },
            new go.Binding("visible", "", function (v, obj) {
                if (obj.part.diagram !== null) {
                    return obj.part.diagram.selection.count > 0;
                } return false;
            }).ofObject()
        ),
        // Flip Dimension Side Button (only available when selection contains Wall Group(s))
        $("ContextMenuButton",
            $(go.TextBlock, "Flip Dimension Side"),
            {
                click: function (e, obj) {
                    var floorplan = obj.part.diagram;
                    if (floorplan !== null) {
                        floorplan.startTransaction("flip dimension link side");
                        var walls = [];
                        floorplan.selection.iterator.each(function (part) {
                            if (part.category === "WallGroup") walls.push(part);
                        });
                        for (var i = 0; i < walls.length; i++) {
                            var wall = walls[i];
                            var sPt = wall.data.startpoint.copy();
                            var ePt = wall.data.endpoint.copy();
                            floorplan.model.setDataProperty(wall.data, "startpoint", ePt);
                            floorplan.model.setDataProperty(wall.data, "endpoint", sPt);
                            floorplan.updateWall(wall);
                        }
                        floorplan.commitTransaction("flip dimension link side");
                    }
                }
            },
            new go.Binding("visible", "", function (v, obj) {
                if (obj.part.diagram !== null) {
                    var sel = obj.part.diagram.selection;
                    if (sel.count === 0) return false;
                    var flag = false;
                    sel.iterator.each(function (part) {
                        if (part.category === "WallGroup") flag = true;
                    });
                    return flag;
                } return false;
            }).ofObject()
        )
   );
}

// Default Group
function makeDefaultGroup() {
    var $ = go.GraphObject.make;
    return $(go.Group, "Vertical",
        {
            contextMenu: makeContextMenu(),
            doubleClick: function (e) {
                if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow");
            },
            toolTip: makeGroupToolTip()
        },
        new go.Binding("location", "loc"),
          $(go.Panel, "Auto",
            $(go.Shape, "RoundedRectangle", { fill: "rgba(128,128,128,0.15)", stroke: 'rgba(128, 128, 128, .05)', name: 'SHAPE', strokeCap: 'square' },
              new go.Binding("fill", "isSelected", function (s, obj) {
                  return s ? "rgba(128, 128, 128, .15)" : "rgba(128, 128, 128, 0.10)";
              }).ofObject()
              ),
            $(go.Placeholder, { padding: 5 })  // extra padding around group members
          )
        )
}

/* 
* Dependencies for Angle Nodes:
* Make Arc
*/

// Return arc geometry for Angle Nodes
function makeArc(node) {
    var ang = node.data.angle;
    var sweep = node.data.sweep;
    var rad = Math.min(30, node.data.maxRadius);
    if (typeof sweep === "number" && sweep > 0) {
        var start = new go.Point(rad, 0).rotate(ang);
        // this is much more efficient than calling go.GraphObject.make:
        return new go.Geometry()
              .add(new go.PathFigure(start.x + rad, start.y + rad)  // start point
                   .add(new go.PathSegment(go.PathSegment.Arc,
                                           ang, sweep,  // angles
                                           rad, rad,  // center
                                           rad, rad)  // radius
                        ))
              .add(new go.PathFigure(0, 0))
              .add(new go.PathFigure(2 * rad, 2 * rad));
    } else {  // make sure this arc always occupies the same circular area of RAD radius
        return new go.Geometry()
              .add(new go.PathFigure(0, 0))
              .add(new go.PathFigure(2 * rad, 2 * rad));
    }
}

/*
* Dependencies for Dimension Links
* Make Point Node
*/

// Return a Point Node (used for Dimension Links)
function makePointNode() {
    var $ = go.GraphObject.make
    return $(go.Node, "Position", new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify));
}

/*
* Dynamically appearing parts: 
* Angle Node, Dimension Link
*/

// Return an Angle Node (for each angle ndeeded in the diagram, one angle node is made)
function makeAngleNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, "Spot",
        { locationSpot: go.Spot.Center, locationObjectName: "SHAPE", selectionAdorned: false },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
            $(go.Shape, "Circle", // placed where walls intersect, is invisible
            { name: "SHAPE", height: 0, width: 0 }),
            $(go.Shape, // arc
            { strokeWidth: 1.5, fill: null }, 
            new go.Binding("geometry", "", makeArc).ofObject(),
            new go.Binding("stroke", "sweep", function (sweep) {
                return (sweep % 45 < 1 || sweep % 45 > 44) ? "dodgerblue" : "lightblue";
            })),
            // Arc label panel
            $(go.Panel, "Auto",
            { name: "ARCLABEL" },
            // position the label in the center of the arc
            new go.Binding("alignment", "sweep", function (sweep, panel) {
                var rad = Math.min(30, panel.part.data.maxRadius);
                var angle = panel.part.data.angle;
                var cntr = new go.Point(rad, 0).rotate(angle + sweep / 2);
                return new go.Spot(0.5, 0.5, cntr.x, cntr.y);
            }),
                // rectangle containing angle text
                $(go.Shape,
                { fill: "white" },
                new go.Binding("stroke", "sweep", function (sweep) {
                    return (sweep % 45 < 1 || sweep % 45 > 44) ? "dodgerblue" : "lightblue";
                })),
                // angle text
                $(go.TextBlock,
                { font: "7pt sans-serif", margin: 2 },
                new go.Binding("text", "sweep", function (sweep) {
                    return sweep.toFixed(2) + String.fromCharCode(176);
                }))
            )
        );
}

// Returns a Dimension Link
function makeDimensionLink() {
    var $ = go.GraphObject.make
    return $(go.Link,
        // link itself
        $(go.Shape,
        { stroke: "gray", strokeWidth: 2, name: 'SHAPE' }),
        // to arrow shape
        $(go.Shape,
        { toArrow: "OpenTriangle", stroke: "gray", strokeWidth: 2 }),
        $(go.Shape,
        // from arrow shape
        { fromArrow: "BackwardOpenTriangle", stroke: "gray", strokeWidth: 2 }),
        // dimension link text 
        $(go.TextBlock,
        { text: 'sometext', segmentOffset: new go.Point(0, -10), font: "13px sans-serif" },
        new go.Binding("text", "", function (link) {
            var floorplan = link.diagram;
            if (floorplan) {
                var fromPtNode = null; var toPtNode = null;
                floorplan.pointNodes.iterator.each(function (node) {
                    if (node.data.key === link.data.from) fromPtNode = node;
                    if (node.data.key === link.data.to) toPtNode = node;
                });
                if (fromPtNode !== null) {
                    var fromPt = fromPtNode.location;
                    var toPt = toPtNode.location;
                    return floorplan.convertPixelsToUnits(Math.sqrt(fromPt.distanceSquaredPoint(toPt))).toFixed(2) + floorplan.model.modelData.unitsAbbreviation;
                } return null;
            } return null;
        }).ofObject(),
        // bind angle of textblock to angle of link -- always make text rightside up and readable
        new go.Binding("angle", "angle", function (angle, link) {
            if (angle > 90 && angle < 270) return (angle + 180) % 360;
            return angle;
        }),
        // default poisiton text above / below dimension link based on angle
        new go.Binding("segmentOffset", "angle", function (angle, textblock) {
            var floorplan = textblock.part.diagram;
            if (floorplan) {
                var wall = floorplan.findPartForKey(textblock.part.data.wall);
                if (wall.rotateObject.angle > 135 && wall.rotateObject.angle < 315) return new go.Point(0, 10);
                return new go.Point(0, -10);
            } return new go.Point(0,0);
        }).ofObject(),
        // scale font size according to the length of the link
        new go.Binding("font", "", function (link) {
            var floorplan = link.diagram;
            var fromPtNode = null; var toPtNode = null;
            floorplan.pointNodes.iterator.each(function (node) {
                if (node.data.key === link.data.from) fromPtNode = node;
                if (node.data.key === link.data.to) toPtNode = node;
            });
            if (fromPtNode !== null) {
                var fromPt = fromPtNode.location;
                var toPt = toPtNode.location;
                var distance = Math.sqrt(fromPt.distanceSquaredPoint(toPt));
                if (distance > 40) return "13px sans-serif";
                if (distance <= 40 && distance >= 20) return "11px sans-serif";
                else return "9px sans-serif";
            } return "13px sans-serif";
        }).ofObject()
        )
    )
}

/*
* Copyright (C) 1998-2018 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - FURNITURE
* GraphObject templates for interactional furniture nodes (and their dependecies) used in the Floor Planner sample   
* Includes Default Node (Furniture), MultiPurpose Node
*/

/*
* Furniture Node Dependencies:
* Node Tool Tip, Furniture Resize Adornment Template, Furniture Rotate Adornment Template, Invert Color
*/

// Node Tool Tip
function makeNodeToolTip() {
    var $ = go.GraphObject.make;
    return $(go.Adornment, "Auto",
            $(go.Shape, { fill: "#FFFFCC" }),
            $(go.TextBlock, { margin: 4 },
            new go.Binding("text", "", function (text, obj) {
                var data = obj.part.adornedObject.data;
                var name = (obj.part.adornedObject.category === "MultiPurposeNode") ? data.text : data.caption;
                return "Name: " + name + "\nNotes: " + data.notes;
            }).ofObject())
        )
}

// Furniture Resize Adornment
function makeFurnitureResizeAdornmentTemplate() {
    var $ = go.GraphObject.make;
    function makeHandle(alignment, cursor) {
        return $(go.Shape, { alignment: alignment, cursor: cursor, figure: "Rectangle", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" },
            new go.Binding("fill", "color"),
            new go.Binding("stroke", "stroke"));
    }

    return $(go.Adornment, "Spot",
      $(go.Placeholder),
      makeHandle(go.Spot.Top, "n-resize"),
      makeHandle(go.Spot.TopRight, "n-resize"),
      makeHandle(go.Spot.BottomRight, "se-resize"),
      makeHandle(go.Spot.Right, "e-resize"),
      makeHandle(go.Spot.Bottom, "s-resize"),
      makeHandle(go.Spot.BottomLeft, "sw-resize"),
      makeHandle(go.Spot.Left, "w-resize"),
      makeHandle(go.Spot.TopLeft, "nw-resize")
    );
}

// Furniture Rotate Adornment
function makeFurnitureRotateAdornmentTemplate() {
    var $ = go.GraphObject.make;
    return $(go.Adornment,
        $(go.Shape, "Circle", { cursor: "pointer", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" },
        new go.Binding("fill", "", function (obj) { return (obj.adornedPart === null) ? "#ffffff" : obj.adornedPart.data.color; }).ofObject(),
        new go.Binding("stroke", "", function (obj) { return (obj.adornedPart === null) ? "#000000" : obj.adornedPart.data.stroke; }).ofObject())
        );
}

// Return inverted color (in hex) of a given hex code color; used to determine furniture node stroke color
function invertColor(hex) {
    if (hex.indexOf('#') === 0) {
        hex = hex.slice(1);
    }
    // convert 3-digit hex to 6-digits.
    if (hex.length === 3) {
        hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
    }
    if (hex.length !== 6) {
        throw new Error('Invalid HEX color.');
    }
    // invert color components
    var r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16),
        g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16),
        b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16);
    // pad each with zeros and return
    return '#' + padZero(r) + padZero(g) + padZero(b);
}

function padZero(str) {
    var len = 2;
    var zeros = new Array(len).join('0');
    return (zeros + str).slice(-len);
}

/*
* Furniture Node Templates:
* Default Node, MultiPurpose Node
*/

// Default Node
function makeDefaultNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, "Spot",
    {
        resizable: true,
        rotatable: true,
        toolTip: makeNodeToolTip(),
        resizeAdornmentTemplate: makeFurnitureResizeAdornmentTemplate(),
        rotateAdornmentTemplate: makeFurnitureRotateAdornmentTemplate(),
        contextMenu: makeContextMenu(),
        locationObjectName: "SHAPE",
        resizeObjectName: "SHAPE",
        rotateObjectName: "SHAPE",
        minSize: new go.Size(5, 5),
        locationSpot: go.Spot.Center,
        selectionAdorned: false,  // use a Binding on the Shape.stroke to show selection
        doubleClick: function (e) {
            if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow")
        }
    },
    // remember Node location
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    // move selected Node to Foreground layer so it's not obscuerd by non-selected Parts
    new go.Binding("layerName", "isSelected", function (s) {
        return s ? "Foreground" : "";
    }).ofObject(),
    $(go.Shape,
    {
        name: "SHAPE", stroke: "#000000",
        fill: "rgba(128, 128, 128, 0.5)"
    },
    new go.Binding("geometryString", "geo"),
    new go.Binding("width").makeTwoWay(),
    new go.Binding("height").makeTwoWay(),
    new go.Binding("angle").makeTwoWay(),
    new go.Binding("fill", "color")),
    new go.Binding("stroke", "isSelected", function (s, obj) {
        return s ? go.Brush.lightenBy(obj.stroke, .5) : invertColor(obj.part.data.color);
    }).ofObject()
  )
}

// MultiPurpose Node
function makeMultiPurposeNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, "Spot",
      {
          contextMenu: makeContextMenu(),
          toolTip: makeNodeToolTip(),
          locationSpot: go.Spot.Center,
          resizeAdornmentTemplate: makeFurnitureResizeAdornmentTemplate(),
          rotateAdornmentTemplate: makeFurnitureRotateAdornmentTemplate(),
          locationObjectName: "SHAPE",
          resizable: true,
          rotatable: true,
          resizeObjectName: "SHAPE",
          rotateObjectName: "SHAPE",
          minSize: new go.Size(5, 5),
          selectionAdorned: false,
          doubleClick: function (e) {
              if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow")
          }
      },
      // remember location, angle, height, and width of the node
      new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
      // move a selected part into the Foreground layer so it's not obscuerd by non-selected Parts
      new go.Binding("layerName", "isSelected", function (s) { return s ? "Foreground" : ""; }).ofObject(),
      $(go.Shape,
      { strokeWidth: 1, name: "SHAPE", fill: "rgba(128, 128, 128, 0.5)", },
      new go.Binding("angle").makeTwoWay(),
      new go.Binding("width").makeTwoWay(),
      new go.Binding("height").makeTwoWay(),
      new go.Binding("fill", "color"),
      new go.Binding("stroke", "isSelected", function (s, obj) {
          return s ? go.Brush.lightenBy(obj.stroke, .5) : invertColor(obj.part.data.color);
      }).ofObject()
      ),
      $(go.TextBlock,
      {
          margin: 5,
          wrap: go.TextBlock.WrapFit,
          textAlign: "center",
          editable: true,
          isMultiline: false,
          stroke: '#454545',
          font: "10pt sans-serif"
      },
      new go.Binding("text").makeTwoWay(),
      new go.Binding("angle", "angle").makeTwoWay(),
      new go.Binding("font", "height", function (height) {
          if (height > 25) return "10pt sans-serif";
          if (height < 25 && height > 15) return "8pt sans-serif";
          else return "6pt sans-serif";
      }),
      new go.Binding("stroke", "color", function (color) { return invertColor(color); })
      )
    )
}

/*
* Copyright (C) 1998-2018 by Northwoods Software Corporation
* All Rights Reserved.
*
* FLOOR PLANNER CODE: TEMPLATES - WALLS
* GraphObject templates for Wall Groups, Wall Part Nodes (and their dependecies) used in the Floor Planner sample   
* Includes Wall Group, Palette Wall Node, Window Node, Door Node
*/

/*
* Wall Group Dependencies:
* Snap Walls, Find Closest Loc on Wall, Add Wall Part, Wall Part Drag Over, Wall Part Drag Away
*/

/*
* Drag computation function to snap walls to the grid properly while dragging
* @param {Node} part A reference to dragged Part
* @param {Point} pt The Point describing the proposed location
* @param {Point} gridPt Snapped location
*/
var snapWalls = function (part, pt, gridPt) {
    var floorplan = part.diagram;
    floorplan.updateWallDimensions();
    floorplan.updateWallAngles();
    floorplan.updateWall(part);
    var grid = part.diagram.grid;
    var sPt = part.data.startpoint.copy();
    var ePt = part.data.endpoint.copy();
    var dx = pt.x - part.location.x;
    var dy = pt.y - part.location.y;
    var newSpt = sPt.offset(dx, dy);
    var newEpt = ePt.offset(dx, dy);
    if (floorplan.toolManager.draggingTool.isGridSnapEnabled) {
        newSpt = newSpt.snapToGridPoint(grid.gridOrigin, grid.gridCellSize);
        newEpt = newEpt.snapToGridPoint(grid.gridOrigin, grid.gridCellSize);
    }
    floorplan.model.setDataProperty(part.data, "startpoint", newSpt);
    floorplan.model.setDataProperty(part.data, "endpoint", newEpt);
    return new go.Point((newSpt.x + newEpt.x) / 2, (newSpt.y + newEpt.y) / 2);
}

/*
* Find closest loc (to mouse point) on wall a wallPart can be dropped onto without extending beyond wall endpoints or intruding into another wallPart
* @param {Group} wall A reference to a Wall Group
* @param {Node} part A reference to a Wall Part Node -- i.e. Door Node, Window Node
*/
function findClosestLocOnWall(wall, part) {
    var orderedConstrainingPts = []; // wall endpoints and wallPart endpoints
    var startpoint = wall.data.startpoint.copy();
    var endpoint = wall.data.endpoint.copy();
    // store all possible constraining endpoints (wall endpoints and wallPart endpoints) in the order in which they appear (left/top to right/bottom)
    var firstWallPt = ((startpoint.x + startpoint.y) <= (endpoint.x + endpoint.y)) ? startpoint : endpoint;
    var lastWallPt = ((startpoint.x + startpoint.y) > (endpoint.x + endpoint.y)) ? startpoint : endpoint;
    var wallPartEndpoints = [];
    wall.memberParts.iterator.each(function (wallPart) {
        var endpoints = getWallPartEndpoints(wallPart);
        wallPartEndpoints.push(endpoints[0]);
        wallPartEndpoints.push(endpoints[1]);
    });
    // sort all wallPartEndpoints by x coordinate left to right
    wallPartEndpoints.sort(function (a, b) {
        if ((a.x + a.y) > (b.x + b.y)) return 1;
        if ((a.x + a.y) < (b.x + b.y)) return -1;
        else return 0;
    });
    orderedConstrainingPts.push(firstWallPt);
    orderedConstrainingPts = orderedConstrainingPts.concat(wallPartEndpoints);
    orderedConstrainingPts.push(lastWallPt);

    // go through all constraining points; if there's a free stretch along the wall "part" could fit in, remember it
    var possibleStretches = [];
    for (var i = 0; i < orderedConstrainingPts.length; i += 2) {
        var point1 = orderedConstrainingPts[i];
        var point2 = orderedConstrainingPts[i + 1];
        var distanceBetween = Math.sqrt(point1.distanceSquaredPoint(point2));
        if (distanceBetween >= part.data.length) possibleStretches.push({ pt1: point1, pt2: point2 });
    }

    // go through all possible stretches along the wall the part *could* fit in; find the one closest to the part's current location
    var closestDist = Number.MAX_VALUE; var closestStretch = null;
    for (var i = 0; i < possibleStretches.length; i++) {
        var testStretch = possibleStretches[i];
        var testPoint1 = testStretch.pt1;
        var testPoint2 = testStretch.pt2;
        var testDistance1 = Math.sqrt(testPoint1.distanceSquaredPoint(part.location));
        var testDistance2 = Math.sqrt(testPoint2.distanceSquaredPoint(part.location));
        if (testDistance1 < closestDist) {
            closestDist = testDistance1;
            closestStretch = testStretch;
        }
        if (testDistance2 < closestDist) {
            closestDist = testDistance2;
            closestStretch = testStretch;
        }
    }

    // Edge Case: If there's no space for the wallPart, return null
    if (closestStretch === null) return null;

    // using the closest free stretch along the wall, calculate endpoints that make the stretch's line segment, then project part.location onto the segment
    var closestStretchLength = Math.sqrt(closestStretch.pt1.distanceSquaredPoint(closestStretch.pt2));
    var offset = part.data.length / 2;
    var pt1 = new go.Point(closestStretch.pt1.x + ((offset / closestStretchLength) * (closestStretch.pt2.x - closestStretch.pt1.x)),
        closestStretch.pt1.y + ((offset / closestStretchLength) * (closestStretch.pt2.y - closestStretch.pt1.y)));
    var pt2 = new go.Point(closestStretch.pt2.x + ((offset / closestStretchLength) * (closestStretch.pt1.x - closestStretch.pt2.x)),
    closestStretch.pt2.y + ((offset / closestStretchLength) * (closestStretch.pt1.y - closestStretch.pt2.y)));
    var newLoc = part.location.copy().projectOntoLineSegmentPoint(pt1, pt2);
    return newLoc;
}

// MouseDrop event for wall groups; if a door or window is dropped on a wall, add it to the wall group
// Do not allow dropping wallParts that would extend beyond wall endpoints or intrude into another wallPart
var addWallPart = function (e, wall) {
    var floorplan = e.diagram;
    var wallPart = floorplan.selection.first();
    if ((wallPart && (wallPart.category === "WindowNode" || wallPart.category === "DoorNode") && wallPart.containingGroup === null)) {
        var newLoc = findClosestLocOnWall(wall, wallPart);
        if (newLoc !== null) {
            wall.findObject("SHAPE").stroke = "black";
            floorplan.model.setDataProperty(wallPart.data, "group", wall.data.key);
            wallPart.location = newLoc.projectOntoLineSegmentPoint(wall.data.startpoint, wall.data.endpoint);
            wallPart.angle = wall.rotateObject.angle;
            if (wallPart.category === "WindowNode") floorplan.model.setDataProperty(wallPart.data, "height", wall.data.thickness);
            if (wallPart.category === "DoorNode") floorplan.model.setDataProperty(wallPart.data, "doorOpeningHeight", wall.data.thickness);
        } else {
            floorplan.remove(wallPart);
            alert("There's not enough room on the wall!");
            return;
        }
    }
    if (floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(floorplan.selection.first(), floorplan);
    floorplan.updateWallDimensions();
}

// MouseDragEnter event for walls; if a door or window is dragged over a wall, highlight the wall and change its angle
var wallPartDragOver = function (e, wall) {
    var floorplan = e.diagram;
    var parts = floorplan.toolManager.draggingTool.draggingParts;
    parts.iterator.each(function (part) {
        if ((part.category === "WindowNode" || part.category === "DoorNode") && part.containingGroup === null) {
            wall.findObject("SHAPE").stroke = "lightblue";
            part.angle = wall.rotateObject.angle;
        }
    });
}

// MouseDragLeave event for walls; if a wall part is dragged past a wall, unhighlight the wall and change back the wall part's angle to 0
var wallPartDragAway = function (e, wall) {
    var floorplan = e.diagram;
    wall.findObject("SHAPE").stroke = "black";
    var parts = floorplan.toolManager.draggingTool.draggingParts;
    parts.iterator.each(function (part) {
        if ((part.category === "WindowNode" || part.category === "DoorNode") && part.containingGroup === null) part.angle = 0
    });
}

/*
* Wall Group Template
*/

// Wall Group
function makeWallGroup() {
    var $ = go.GraphObject.make;
    return $(go.Group, "Spot",
        {
            contextMenu: makeContextMenu(),
            toolTip: makeGroupToolTip(),
            selectionObjectName: "SHAPE",
            rotateObjectName: "SHAPE",
            locationSpot: go.Spot.Center,
            reshapable: true,
            minSize: new go.Size(1, 1),
            dragComputation: snapWalls,
            selectionAdorned: false,
            mouseDrop: addWallPart,
            mouseDragEnter: wallPartDragOver,
            mouseDragLeave: wallPartDragAway,
            doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); }
        },
        $(go.Shape,
        {
            name: "SHAPE",
            fill: "black",
        },
        new go.Binding("strokeWidth", "thickness"),
        new go.Binding("stroke", "isSelected", function (s, obj) {
            if (obj.part.containingGroup != null) {
                var group = obj.part.containingGroup;
                if (s) { group.data.isSelected = true; }
            }
            return s ? "dodgerblue" : "black";
        }).ofObject()
      ))
}

/*
* Wall Part Node Dependencies:
* Get Wall Part Endpoints, Get Wall Part Stretch, Drag Wall Parts (Drag Computation Function),
* Wall Part Resize Adornment, Door Selection Adornment (Door Nodes only)
*/

/*
* Find and return an array of the endpoints of a given wallpart (window or door)
* @param {Node} wallPart A Wall Part Node -- i.e. Door Node, Window Node
*/
function getWallPartEndpoints(wallPart) {
    var loc = wallPart.location;
    var partLength = wallPart.data.length; var angle = 0;
    if (wallPart.containingGroup !== null) angle = wallPart.containingGroup.rotateObject.angle;
    else angle = 180;
    var point1 = new go.Point((loc.x + (partLength / 2)), loc.y);
    var point2 = new go.Point((loc.x - (partLength / 2)), loc.y);
    point1.offset(-loc.x, -loc.y).rotate(angle).offset(loc.x, loc.y);
    point2.offset(-loc.x, -loc.y).rotate(angle).offset(loc.x, loc.y);
    var arr = []; arr.push(point1); arr.push(point2);
    return arr;
}

/*
* Returns a "stretch" (2 Points) that constrains a wallPart (door or window), comprised of "part"'s containing wall endpoints or other wallPart endpoints
* @param {Node} part A Wall Part Node -- i.e. Door Node, Window Node, that is attached to a wall
*/
function getWallPartStretch(part) {
    var wall = part.containingGroup;
    var startpoint = wall.data.startpoint.copy();
    var endpoint = wall.data.endpoint.copy();

    // sort all possible endpoints into either left/above or right/below
    var leftOrAbove = new go.Set(/*go.Point*/); var rightOrBelow = new go.Set(/*go.Point*/);
    wall.memberParts.iterator.each(function (wallPart) {
        if (wallPart.data.key !== part.data.key) {
            var endpoints = getWallPartEndpoints(wallPart);
            for (var i = 0; i < endpoints.length; i++) {
                if (endpoints[i].x < part.location.x || (endpoints[i].y > part.location.y && endpoints[i].x === part.location.x)) leftOrAbove.add(endpoints[i]);
                else rightOrBelow.add(endpoints[i]);
            }
        }
    });

    // do the same with the startpoint and endpoint of the dragging part's wall
    if (parseFloat(startpoint.x.toFixed(2)) < parseFloat(part.location.x.toFixed(2)) || (startpoint.y > part.location.y && parseFloat(startpoint.x.toFixed(2)) === parseFloat(part.location.x.toFixed(2)))) leftOrAbove.add(startpoint);
    else rightOrBelow.add(startpoint);
    if (parseFloat(endpoint.x.toFixed(2)) < parseFloat(part.location.x.toFixed(2)) || (endpoint.y > part.location.y && parseFloat(endpoint.x.toFixed(2)) === parseFloat(part.location.x.toFixed(2)))) leftOrAbove.add(endpoint);
    else rightOrBelow.add(endpoint);

    // of each set, find the closest point to the dragging part
    var leftOrAbovePt; var closestDistLeftOrAbove = Number.MAX_VALUE;
    leftOrAbove.iterator.each(function (point) {
    	let pt: go.Point = <go.Point>point;
        var distance = Math.sqrt(pt.distanceSquaredPoint(part.location));
        if (distance < closestDistLeftOrAbove) {
            closestDistLeftOrAbove = distance;
            leftOrAbovePt = pt;
        }
    });
    var rightOrBelowPt; var closestDistRightOrBelow = Number.MAX_VALUE;
    rightOrBelow.iterator.each(function (point) {
    	let pt: go.Point = <go.Point>point;
        var distance = Math.sqrt(pt.distanceSquaredPoint(part.location));
        if (distance < closestDistRightOrBelow) {
            closestDistRightOrBelow = distance;
            rightOrBelowPt = pt;
        }
    });

    var stretch = { point1: leftOrAbovePt, point2: rightOrBelowPt };
    return stretch;
}

/*
* Drag computation function for WindowNodes and DoorNodes; ensure wall parts stay in walls when dragged
* @param {Node} part A reference to dragged Part
* @param {Point} pt The Point describing the proposed location
* @param {Point} gridPt Snapped location
*/
var dragWallParts = function (part, pt, gridPt) {
    if (part.containingGroup !== null && part.containingGroup.category === 'WallGroup') {
        var floorplan = part.diagram;
        // Edge Case: if part is not on its wall (due to incorrect load) snap part.loc onto its wall immediately; ideally this is never called
        var wall = part.containingGroup;
        var wStart = wall.data.startpoint;
        var wEnd = wall.data.endpoint;
        var dist1 = Math.sqrt(wStart.distanceSquaredPoint(part.location));
        var dist2 = Math.sqrt(part.location.distanceSquaredPoint(wEnd));
        var totalDist = Math.sqrt(wStart.distanceSquaredPoint(wEnd));
        if (dist1 + dist2 !== totalDist) part.location = part.location.copy().projectOntoLineSegmentPoint(wStart, wEnd);

        // main behavior
        var stretch = getWallPartStretch(part);
        var leftOrAbovePt = stretch.point1;
        var rightOrBelowPt = stretch.point2;

        // calc points along line created by the endpoints that are half the width of the moving window/door
        var totalLength = Math.sqrt(leftOrAbovePt.distanceSquaredPoint(rightOrBelowPt));
        var distance = (part.data.length / 2);
        var point1 = new go.Point(leftOrAbovePt.x + ((distance / totalLength) * (rightOrBelowPt.x - leftOrAbovePt.x)),
        leftOrAbovePt.y + ((distance / totalLength) * (rightOrBelowPt.y - leftOrAbovePt.y)));
        var point2 = new go.Point(rightOrBelowPt.x + ((distance / totalLength) * (leftOrAbovePt.x - rightOrBelowPt.x)),
        rightOrBelowPt.y + ((distance / totalLength) * (leftOrAbovePt.y - rightOrBelowPt.y)));

        // calc distance from pt to line (part's wall) - use point to 2pt line segment distance formula
        var distFromWall = Math.abs(((wEnd.y - wStart.y) * pt.x) - ((wEnd.x - wStart.x) * pt.y) + (wEnd.x * wStart.y) - (wEnd.y * wStart.x)) /
            Math.sqrt(Math.pow((wEnd.y - wStart.y), 2) + Math.pow((wEnd.x - wStart.x), 2));
        var tolerance = (20 * wall.data.thickness < 100) ? (20 * wall.data.thickness) : 100;

        // if distance from pt to line > some tolerance, detach the wallPart from the wall
        if (distFromWall > tolerance) {
            part.containingGroup = null;
            delete part.data.group;
            part.angle = 0;
            floorplan.pointNodes.iterator.each(function (node) { floorplan.remove(node) });
            floorplan.dimensionLinks.iterator.each(function (link) { floorplan.remove(link) });
            floorplan.pointNodes.clear();
            floorplan.dimensionLinks.clear();
            floorplan.updateWallDimensions();
            if (floorplan.floorplanUI) floorplan.floorplanUI.setSelectionInfo(part);
        }

        // project the proposed location onto the line segment created by the new points (ensures wall parts are constrained properly when dragged)
        pt = pt.copy().projectOntoLineSegmentPoint(point1, point2);
        floorplan.skipsUndoManager = true;
        floorplan.startTransaction("set loc");
        floorplan.model.setDataProperty(part.data, "loc", go.Point.stringify(pt));
        floorplan.commitTransaction("set loc");
        floorplan.skipsUndoManager = false;

        floorplan.updateWallDimensions(); // update the dimension links created by having this wall part selected
    } return pt;
}

// Resize Adornment for Wall Part Nodes
function makeWallPartResizeAdornment() {
    var $ = go.GraphObject.make;
    return $(go.Adornment, "Spot",
    { name: "WallPartResizeAdornment" },
    $(go.Placeholder),
    $(go.Shape, { alignment: go.Spot.Left, cursor: "w-resize", figure: "Diamond", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" }),
    $(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", figure: "Diamond", desiredSize: new go.Size(7, 7), fill: "#ffffff", stroke: "#808080" })
  );
}

// Selection Adornment for Door Nodes
function makeDoorSelectionAdornment() {
    var $ = go.GraphObject.make;
    return $(go.Adornment, "Vertical",
        { name: "DoorSelectionAdornment" },
        $(go.Panel, "Auto",
        $(go.Shape, { fill: null, stroke: null }),
        $(go.Placeholder)),
        $(go.Panel, "Horizontal", { defaultStretch: go.GraphObject.Vertical },
            $("Button",
                $(go.Picture, { source: "icons/flipDoorOpeningLeft.png", column: 0, desiredSize: new go.Size(12, 12) },
                    new go.Binding("source", "", function (obj) {
                        if (obj.adornedPart === null) return "icons/flipDoorOpeningRight.png";
                        else if (obj.adornedPart.data.swing === "left") return "icons/flipDoorOpeningRight.png";
                        else return "icons/flipDoorOpeningLeft.png";
                    }).ofObject()
                ),
                {
                    click: function (e, obj) {
                        var floorplan = obj.part.diagram;
                        floorplan.startTransaction("flip door");
                        var door = obj.part.adornedPart;
                        if (door.data.swing === "left") floorplan.model.setDataProperty(door.data, "swing", "right");
                        else floorplan.model.setDataProperty(door.data, "swing", "left");
                        floorplan.commitTransaction("flip door");
                    },
                    toolTip: $(go.Adornment, "Auto",
                        $(go.Shape, { fill: "#FFFFCC" }),
                        $(go.TextBlock, { margin: 4, text: "Flip Door Opening" }
                    ))
                },
                new go.Binding("visible", "", function (obj) { return (obj.adornedPart === null) ? false : (obj.adornedPart.containingGroup !== null); }).ofObject()
             ),
             $("Button",
                $(go.Picture, { source: "icons/flipDoorSide.png", column: 0, desiredSize: new go.Size(12, 12) }),
                {
                    click: function (e, obj) {
                        var floorplan = obj.part.diagram;
                        floorplan.startTransaction("rotate door");
                        var door = obj.part.adornedPart;
                        door.angle = (door.angle + 180) % 360;
                        floorplan.commitTransaction("rotate door");
                    },
                    toolTip: $(go.Adornment, "Auto",
                        $(go.Shape, { fill: "#FFFFCC" }),
                        $(go.TextBlock, { margin: 4, text: "Flip Door Side" }
                    ))
                }
             ),
             new go.Binding("visible", "", function (obj) { return (obj.adornedPart === null) ? false : (obj.adornedPart.containingGroup !== null); }).ofObject()
        )
      );
}

/*
* Wall Part Nodes:
* Window Node, Door Node, Palette Wall Node
*/

// Window Node
function makeWindowNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, "Spot",
        {
            contextMenu: makeContextMenu(),
            selectionObjectName: "SHAPE",
            selectionAdorned: false,
            locationSpot: go.Spot.Center,
            toolTip: makeNodeToolTip(),
            minSize: new go.Size(5, 5),
            resizable: true,
            resizeAdornmentTemplate: makeWallPartResizeAdornment(),
            resizeObjectName: "SHAPE",
            rotatable: false,
            doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); },
            dragComputation: dragWallParts,
            layerName: 'Foreground' // make sure windows are always in front of walls
        },
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        new go.Binding("angle").makeTwoWay(),
        $(go.Shape,
        { name: "SHAPE", fill: "white", strokeWidth: 0 },
        new go.Binding("width", "length").makeTwoWay(),
        new go.Binding("height").makeTwoWay(),
        new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
        new go.Binding("fill", "isSelected", function (s, obj) { return s ? "lightgray" : "white"; }).ofObject()
        ),
        $(go.Shape,
        { name: "LINESHAPE", fill: "darkgray", strokeWidth: 0, height: 10 },
        new go.Binding("width", "length", function (width, obj) { return width - 10; }), // 5px padding each side
        new go.Binding("height", "height", function (height, obj) { return (height / 5); }),
        new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject()
        )
      );
}

// Door Node
function makeDoorNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, "Spot",
        {
            contextMenu: makeContextMenu(),
            selectionObjectName: "SHAPE",
            selectionAdornmentTemplate: makeDoorSelectionAdornment(),
            locationSpot: go.Spot.BottomCenter,
            resizable: true,
            resizeObjectName: "OPENING_SHAPE",
            toolTip: makeNodeToolTip(),
            minSize: new go.Size(10, 10),
            doubleClick: function (e) { if (e.diagram.floorplanUI) e.diagram.floorplanUI.hideShow("selectionInfoWindow"); },
            dragComputation: dragWallParts,
            resizeAdornmentTemplate: makeWallPartResizeAdornment(),
            layerName: 'Foreground' // make sure windows are always in front of walls
        },
        // remember location of the Node
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        new go.Binding("angle").makeTwoWay(),
        // the door's locationSpot is affected by it's openingHeight, which is affected by the thickness of its containing wall
        new go.Binding("locationSpot", "doorOpeningHeight", function (doh, obj) { return new go.Spot(0.5, 1, 0, -(doh / 2)); }),
        // this is the shape that reprents the door itself and its swing
        $(go.Shape,
        { name: "SHAPE" },
        new go.Binding("width", "length"),
        new go.Binding("height", "length").makeTwoWay(),
        new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
        new go.Binding("fill", "color"),
        new go.Binding("geometryString", "swing", function (swing) {
            if (swing === "left") return "F1 M0,0 v-150 a150,150 0 0,1 150,150 ";
            else return "F1 M275,175 v-150 a150,150 0 0,0 -150,150 ";
        })
        ),
        // door opening shape
        $(go.Shape,
        {
            name: "OPENING_SHAPE", fill: "white",
            strokeWidth: 0, height: 5, width: 40,
            alignment: go.Spot.BottomCenter, alignmentFocus: go.Spot.Center
        },
        new go.Binding("height", "doorOpeningHeight").makeTwoWay(),
        new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
        new go.Binding("fill", "isSelected", function (s, obj) { return s ? "lightgray" : "white"; }).ofObject(),
        new go.Binding("width", "length").makeTwoWay()
        )
      );
}

// Palette Wall Node (becomes WallGroup when dropped from Palette onto diagram)
function makePaletteWallNode() {
    var $ = go.GraphObject.make;
    return $(go.Node, "Spot",
        { selectionAdorned: false },
        $(go.Shape,
        { name: "SHAPE", fill: "black", strokeWidth: 0, height: 10, figure: "Rectangle" },
        new go.Binding("width", "length").makeTwoWay(),
        new go.Binding("height").makeTwoWay(),
        new go.Binding("fill", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject(),
        new go.Binding("stroke", "isSelected", function (s, obj) { return s ? "dodgerblue" : "black"; }).ofObject())
    );
}

export = Floorplan;