API Docs for: 2.0.0

src/node-partition.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2015

/**
  * Partitioning of SAGE2 Apps into groups
  * @module server
  * @submodule Partition
  */

// require variables to be declared
"use strict";


var StickyItems         = require('./node-stickyitems');
var stickyAppHandler     = new StickyItems();

/**
  * @class Partition
  * @constructor
  */
function Partition(dims, id, color, partitionList) {
	// the list which this partition is a part of
	this.partitionList = partitionList;

	this.children = {};
	this.numChildren = 0;

	this.width = dims.width;
	this.height = dims.height;
	this.left = dims.left;
	this.top = dims.top;
	this.aspect = dims.width / dims.height;

	this.previous_left = null;
	this.previous_top = null;
	this.previous_width = null;
	this.previous_height = null;

	this.resizeMode = "free";
	this.maximized = false;

	this.id = id;
	this.color = color;

	this.bounding = true;

	this.innerTiling = false;
	this.currentMaximizedChild = null;

	// for the more geometric idea of partitions
	this.isSnapping = dims.isSnapping || false;
}

/**
  * Add a Child application to the partition.
  *
  * @param {object} item - The item to be added
  */
Partition.prototype.addChild = function(item) {
	var changedPartitions = [];

	if (item.partition /*&& item.partition !== this*/) {
		// if the item was already in another partition, remove and add to this partition
		changedPartitions.push(item.partition.id);
		this.partitionList.removeChildFromPartition(item.id, item.partition.id);
	}

	changedPartitions.push(this.id);
	item.partition = this;

	// if item is bigger than or slightly outside of partition, resize and put inside
	// to show that the item is now in the partiton
	var titleBarHeight = this.partitionList.configuration.ui.titleBarHeight;

	if (item.width > this.width - 8) {
		item.width = this.width - 8;
		item.height = item.width / item.aspect;
	}
	if (item.height > this.height - titleBarHeight - 8) {
		item.height = this.height - titleBarHeight - 8;
		item.width = item.height * item.aspect;
	}

	if (item.left < this.left + 4) {
		item.left = this.left + 4;
	} else if (item.left + item.width > this.left + this.width - 4) {
		item.left = this.left + this.width - 4 - item.width;
	}

	if (item.top < this.top + titleBarHeight + 4) {
		item.top = this.top + titleBarHeight + 4;
	} else if (item.top + item.height > this.top + this.height - 4) {
		item.top = this.top + this.height - 4 - item.height;
	}

	// save positions within partition as percentages of partition
	item.relative_left = (item.left - this.left) / this.width;
	item.relative_top = (item.top - this.top - titleBarHeight) / this.height;
	item.relative_width = item.width / this.width;
	item.relative_height = item.height / this.height;

	this.numChildren++;
	this.children[item.id] = item;

	if (this.innerTiling && this.currentMaximizedChild) {
		this.maximizeChild(item.id);
	}

	var stickingItems = stickyAppHandler.getFirstLevelStickingItems(item);
	for (var s in stickingItems) {
		this.addChild(stickingItems[s]);
	}
	return changedPartitions;
};

Partition.prototype.updateChild = function(id) {
	if (this.children.hasOwnProperty(id)) {
		// when a child is moved, update the relative positions within the parent
		var item = this.children[id];
		var titleBarHeight = this.partitionList.configuration.ui.titleBarHeight;

		item.relative_left = (item.left - this.left) / this.width;
		item.relative_top = (item.top - this.top - titleBarHeight) / this.height;
		item.relative_width = item.width / this.width;
		item.relative_height = item.height / this.height;
		var movedItems = stickyAppHandler.moveFirstLevelItemsStickingToUpdatedItem(item);
		for (var mI in movedItems) {
			this.updateChild(movedItems[mI].elemId);
		}
	}

	return [this.id];
};

/**
  * Remove a Child application to the partition.
  *
  * @param {string} id - The id of child to remove
  */
Partition.prototype.releaseChild = function(id) {
	if (this.children.hasOwnProperty(id)) {

		var item = this.children[id];


		item.relative_left = null;
		item.relative_top = null;
		item.relative_width = null;
		item.relative_height = null;

		item.maximized = false;
		item.partition = null;

		this.numChildren--;
		delete this.children[id];

		// if it is tiling and maximized, maximize another child
		if (this.innerTiling && this.currentMaximizedChild === id) {

			if (Object.keys(this.children).length > 0) {
				this.maximizeChild(Object.keys(this.children)[0]);
			} else {
				this.currentMaximizedChild = null;
			}
		}
		var stickingItems = stickyAppHandler.getFirstLevelStickingItems(item);
		for (var s in stickingItems) {
			this.releaseChild(stickingItems[s].id);
		}
	}

	return [this.id];
};

/**
  * Remove all Child applications from the partition
  */
Partition.prototype.releaseAllChildren = function() {
	var childIDs = Object.keys(this.children);


	childIDs.forEach((cID) => {
		this.releaseChild(cID);
	});

	return [this.id];
};

/**
  * Delete all children within the partition
  */
Partition.prototype.clearPartition = function(deleteFnc) {
	var childIDs = Object.keys(this.children);

	childIDs.forEach((el) => {
		this.releaseChild(el);
		deleteFnc(el);
	});

	return [this.id];
};

/**
  * Toggle partition tiling mode
  *
  */
Partition.prototype.toggleInnerTiling = function() {
	this.innerTiling = !this.innerTiling;

	if (this.innerTiling) {
		this.tilePartition();
	} else {
		this.currentMaximizedChild = null;
	}

	return [this.id];
};

/**
  * Set partition background color
  */
Partition.prototype.setColor = function (color) {
	this.color = color;
};



/**
  * Re-tile the apps within a partition
	* Tiling Algorithm taken from server.js
  */
Partition.prototype.tilePartition = function() {

	// alias for this
	var _this = this;

	var app;
	var i, c, r, key;
	var numCols, numRows, numCells;

	var backgroundAndForegroundItems = stickyAppHandler.getListOfBackgroundAndForegroundItems(this.children);
	var appsWithoutBackground = backgroundAndForegroundItems.backgroundItems;
	var numAppsWithoutBackground = appsWithoutBackground.length;

	// Don't use sticking items to compute number of windows.
	var numWindows = numAppsWithoutBackground - (this.currentMaximizedChild ? 1 : 0);


	// determine the bounds of the tiling area
	var titleBar = this.partitionList.configuration.ui.titleBarHeight;
	// if (this.partitionList.configuration.ui.auto_hide_ui === true) {
	// 	titleBar = 0;
	// }

	var tilingArea = {
		left: this.left,
		top: this.top + titleBar,
		width: this.width,
		height: this.height
	};

	var maxChildCopy = null;

	if (this.currentMaximizedChild) {

		let maxChild = this.children[this.currentMaximizedChild];

		if (numWindows === 0) {
			// if the maximized window is the only window
			// place in center
			maxChild.left = this.left + this.width / 2 - maxChild.width / 2;
			maxChild.top = this.top + (this.height - maxChild.height + titleBar) / 2;

			this.updateChild(this.currentMaximizedChild);
			return;
		}

		if (maxChild.maximizeConstraint === "width_ptn") {
			// aspect ratio is wider than partition

			// shift maximized child to top
			maxChild.top = this.top + 2 * titleBar;

			// adjust tiling area to be rest of space
			tilingArea.top = maxChild.top + maxChild.height;
			tilingArea.height = this.height - maxChild.height - titleBar;
		} else if (maxChild.maximizeConstraint === "height_ptn") {
			// aspect ratio is taller than partition

			// shift maximized child to left
			maxChild.left = this.left + 4;

			// adjust tiling area to be rest of space
			tilingArea.left = maxChild.left + maxChild.width;
			tilingArea.width = this.width - maxChild.width;
		}

		// shift maximized child to top-left
		maxChild.top = this.top + titleBar + 4;
		maxChild.left = this.left + 4;

		maxChildCopy = Object.assign({}, maxChild);
		this.updateChild(this.currentMaximizedChild);
	}

	if (numWindows === 0) {
		// return if no windows
		return;
	}

	var displayAr  = tilingArea.width / tilingArea.height;
	var arDiff     = displayAr / averageWindowAspectRatio();

	// 3 scenarios... windows are on average the same aspect ratio as the display
	if (arDiff >= 0.7 && arDiff <= 1.3) {
		numCols = Math.ceil(Math.sqrt(numWindows));
		numRows = Math.ceil(numWindows / numCols);
	} else if (arDiff < 0.7) {
		// windows are much wider than display
		c = Math.round(1 / (arDiff / 2.0));
		if (numWindows <= c) {
			numRows = numWindows;
			numCols = 1;
		} else {
			numCols = Math.max(2, Math.round(numWindows / c));
			numRows = Math.round(Math.ceil(numWindows / numCols));
		}
	} else {
		// windows are much taller than display
		c = Math.round(arDiff * 2);
		if (numWindows <= c) {
			numCols = numWindows;
			numRows = 1;
		} else {
			numRows = Math.max(2, Math.round(numWindows / c));
			numCols = Math.round(Math.ceil(numWindows / numRows));
		}
	}
	numCells = numRows * numCols;

	var areaX = 0;
	var areaY = Math.round(1.5 * titleBar); // keep 0.5 height as margin
	// if (this.partitionList.configuration.ui.auto_hide_ui === true) {
	// 	areaY = -this.partitionList.configuration.ui.titleBarHeight;
	// }

	var areaW = tilingArea.width;
	var areaH = tilingArea.height - (1.0 * titleBar);

	var tileW = Math.floor(areaW / numCols);
	var tileH = Math.floor(areaH / numRows);

	var padding = 4;
	// if only one application, no padding, i.e maximize
	if (numWindows === 1) {
		padding = 0;
	}

	var centroidsApps  = {};
	var centroidsTiles = [];

	// Caculate apps centers
	// use a subset of children excluding maximizedChild

	for (key in appsWithoutBackground) {
		app = appsWithoutBackground[key];
		centroidsApps[key] = {x: app.left + app.width / 2.0, y: app.top + app.height / 2.0};
	}
	// Caculate tiles centers
	for (i = 0; i < numCells; i++) {
		c = i % numCols;
		r = Math.floor(i / numCols);
		centroidsTiles.push({x: (c * tileW + areaX) + tileW / 2.0, y: (r * tileH + areaY) + tileH / 2.0});
	}

	// Calculate distances
	var distances = {};
	for (key in centroidsApps) {
		distances[key] = [];
		for (i = 0; i < numCells; i++) {
			var d = distanceSquared2D(centroidsApps[key], centroidsTiles[i]);
			distances[key].push(d);
		}
	}

	for (key in appsWithoutBackground) {
		// get the application
		app = appsWithoutBackground[key];

		// pick a cell
		var cellid = findMinimum(distances[key]);
		// put infinite value to disable the chosen cell
		for (i in appsWithoutBackground) {
			distances[i][cellid] = Number.MAX_VALUE;
		}

		// calculate new dimensions
		c = cellid % numCols;
		r = Math.floor(cellid / numCols);
		var newdims = fitWithin(app, c * tileW + areaX, r * tileH + areaY, tileW, tileH, padding);

		// update the data structure
		app.left = newdims[0] + tilingArea.left;
		app.top = newdims[1] - titleBar + tilingArea.top - titleBar / 2;
		app.width = newdims[2];
		app.height = newdims[3];

		// var updateItem = {
		// 	elemId: app.id,
		// 	elemLeft: app.left,
		// 	elemTop: app.top,
		// 	elemWidth: app.width,
		// 	elemHeight: app.height,
		// 	force: true,
		// 	date: Date.now()
		// };

		// broadcast('startMove', {id: updateItem.elemId, date: updateItem.date});
		// broadcast('startResize', {id: updateItem.elemId, date: updateItem.date});
		stickyAppHandler.pileItemsStickingToUpdatedItem(app);
		this.updateChild(app.id);
		// broadcast('finishedMove', {id: updateItem.elemId, date: updateItem.date});
		// broadcast('finishedResize', {id: updateItem.elemId, date: updateItem.date});
	}
	//Restore maximized app's dimensions and position
	if (this.currentMaximizedChild) {
		let maxChild = this.children[this.currentMaximizedChild];
		maxChild.left = maxChildCopy.left;
		maxChild.top = maxChildCopy.top;
		maxChild.width = maxChildCopy.width;
		maxChild.height = maxChildCopy.height;
		this.updateChild(this.currentMaximizedChild);
	}

	function averageWindowAspectRatio() {
		var num = _this.numChildren;

		if (num === 0) {
			return 1.0;
		}

		var totAr = 0.0;
		var key;
		for (key in _this.children) {
			totAr += (_this.children[key].width / _this.children[key].height);
		}
		return (totAr / num);
	}

	function fitWithin(app, x, y, width, height, margin) {
		var titleBar = _this.partitionList.configuration.ui.titleBarHeight;
		// if (_this.partitionList.configuration.ui.auto_hide_ui === true) {
		// 	titleBar = 0;
		// }

		// take buffer into account
		x += margin;
		y += margin;
		width  = width  - 2 * margin;
		height = height - 2 * margin;

		var widthRatio  = (width - titleBar)  / app.width;
		var heightRatio = (height - titleBar) / app.height;
		var maximizeRatio;
		if (widthRatio > heightRatio) {
			maximizeRatio = heightRatio;
		} else {
			maximizeRatio = widthRatio;
		}

		// figure out the maximized app size (w/o the widgets)
		var newAppWidth  = Math.round(maximizeRatio * app.width);
		var newAppHeight = Math.round(maximizeRatio * app.height);

		// figure out the maximized app position (with the widgets)
		var postMaxX = Math.round(width / 2.0 - newAppWidth / 2.0);
		var postMaxY = Math.round(height / 2.0 - newAppHeight / 2.0);

		// the new position of the app considering the maximized state and
		// all the widgets around it
		var newAppX = x + postMaxX;
		var newAppY = y + postMaxY;

		return [newAppX, newAppY, newAppWidth, newAppHeight];
	}

	// Calculate the square of euclidian distance between two objects with .x and .y fields
	function distanceSquared2D(p1, p2) {
		var dx = p2.x - p1.x;
		var dy = p2.y - p1.y;
		return (dx * dx + dy * dy);
	}

	function findMinimum(arr) {
		var val = Number.MAX_VALUE;
		var idx = 0;
		for (var i = 0; i < arr.length; i++) {
			if (arr[i] < val) {
				val = arr[i];
				idx = i;
			}
		}
		return idx;
	}
};

/**
  * Maximize a specific child in the partition
  *
  * @param {string} id - The id of child to maximize
  */
Partition.prototype.maximizeChild = function(id, shift) {
	if (this.children.hasOwnProperty(id)) {
		var item = this.children[id];
		var config = this.partitionList.configuration;

		// normally is just the screen size, but if in partition it is the partition boundaries
		var titleBar = config.ui.titleBarHeight;
		// if (config.ui.auto_hide_ui === true) {
		// 	titleBar = 0;
		// }

		// only 1 child maximized in tiled mode
		if (this.innerTiling) {
			if (this.currentMaximizedChild) {
				this.restoreChild(this.currentMaximizedChild);
			}
			this.currentMaximizedChild = id;
		}

		var maxBound = {
			left: this.left + 4,
			top: this.top + 4,
			width: this.width - 8,
			height: this.height - 8
		};

		var outerRatio = maxBound.width  / maxBound.height;
		var iCenterX  = item.left + item.width / 2.0;
		var iCenterY  = item.top + item.height / 2.0;
		var iWidth    = 1;
		var iHeight   = 1;


		if (shift === true && item.resizeMode === "free") {
			// previously would resize to native height/width
			// item.aspect = item.native_width / item.native_height;

			// Free Resize aspect ratio fills wall
			iWidth = maxBound.width;
			iHeight = maxBound.height - titleBar;
			item.maximizeConstraint = "none_ptn";
		} else {
			if (item.aspect > outerRatio) {
				// Image wider than wall area
				iWidth  = maxBound.width;
				iHeight = iWidth / item.aspect;
				item.maximizeConstraint = "width_ptn";
			} else {
				// Wall area than image
				iHeight = maxBound.height - titleBar;
				iWidth  = iHeight * item.aspect;
				item.maximizeConstraint = "height_ptn";
			}
		}

		// back up values for restore
		item.previous_left   = item.left;
		item.previous_top    = item.top;
		item.previous_width  = item.width;
		item.previous_height = item.width / item.aspect;

		item.previous_relative_left = item.relative_left;
		item.previous_relative_top = item.relative_top;
		item.previous_relative_width = item.relative_width;
		item.previous_relative_height = item.relative_height;

		// calculate new values
		item.top    = iCenterY - (iHeight / 2);
		item.width  = iWidth;
		item.height = iHeight;

		// keep window inside display horizontally
		if (iCenterX - (iWidth / 2) < maxBound.left) {
			item.left = maxBound.left;
		} else if (iCenterX + (iWidth / 2) > maxBound.left + maxBound.width) {
			item.left = maxBound.width + maxBound.left - iWidth;
		} else {
			item.left = iCenterX - (iWidth / 2);
		}

		// keep window inside display vertically
		if (iCenterY - (iHeight / 2) < maxBound.top + titleBar) {
			item.top = maxBound.top + titleBar;
		} else if (iCenterY + (iHeight / 2) > maxBound.top + (maxBound.height + titleBar)) {
			item.top = maxBound.top + maxBound.height - iHeight;
		} else {
			item.top = iCenterY - (iHeight / 2);
		}

		// Shift by 'titleBarHeight' if no auto-hide
		// if (this.partitionList.configuration.ui.auto_hide_ui === true) {
		// 	item.top = item.top - this.partitionList.configuration.ui.titleBarHeight;
		// }

		this.updateChild(item.id);

		item.maximized = true;

		return {
			elemId: item.id, elemLeft: item.left, elemTop: item.top,
			elemWidth: item.width, elemHeight: item.height, date: new Date()
		};
	}

	return null;
};

/**
  * Restore a specific child in the partition
  *
  * @param {string} id - The id of child to restore
  */
Partition.prototype.restoreChild = function(id, shift) {
	if (this.children.hasOwnProperty(id)) {
		var item = this.children[id];

		this.currentMaximizedChild = null;

		if (shift === true) {
			// resize to native width/height
			item.aspect = item.native_width / item.native_height;
			item.left = item.previous_left + item.previous_width / 2 - item.native_width / 2;
			item.top = item.previous_top + item.previous_height / 2 - item.native_height / 2;
			item.width = item.native_width;
			item.height = item.native_height;
		} else {
			item.left   = item.previous_relative_left * this.width + this.left;
			item.top    = item.previous_relative_top * this.height +
				this.top + this.partitionList.configuration.ui.titleBarHeight;
			item.width  = item.previous_relative_width * this.width;
			item.height = item.previous_relative_height * this.height;
		}

		this.updateChild(item.id);

		item.maximized = false;

		return {
			elemId: item.id, elemLeft: item.left, elemTop: item.top,
			elemWidth: item.width, elemHeight: item.height, date: new Date()
		};
	}

	return null;
};

/**
  * Updates the inner layout of the Partition according to whether or not
	* the partition is in innerTiling mode or has a maximized child mode or both
  *
  * @param {string} id - The id of child to restore
  */
Partition.prototype.updateInnerLayout = function() {
	if (this.currentMaximizedChild) {
		this.maximizeChild(this.currentMaximizedChild);
	}

	if (this.innerTiling) {
		this.tilePartition();
	}
};


/**
  * Updates positions of all children based on the relative positions of the children
	* within the Partition and the size of the Partition. Used when a Partition is moved
	* or resized and the children should also resize/move. Returns a list of children
	* which have been moved.
  *
  */
Partition.prototype.updateChildrenPositions = function() {
	var updatedChildren = [];

	var backgroundAndForegroundItems = stickyAppHandler.getListOfBackgroundAndForegroundItems(this.children);
	var appsWithoutBackground = backgroundAndForegroundItems.backgroundItems;
	var titleBarHeight = this.partitionList.configuration.ui.titleBarHeight;

	appsWithoutBackground.forEach((item) => {
		item.left = item.relative_left * this.width + this.left;
		item.top = item.relative_top * this.height + this.top + titleBarHeight;
		item.width = item.relative_width * this.width;
		item.height = item.relative_height * this.height;

		if (item.resizeMode !== "free") {
			if (item.aspect < item.width / item.height) {
				item.width = item.height * item.aspect;
			} else {
				item.height = item.width / item.aspect;
			}
		}

		updatedChildren.push({
			elemId: item.id, elemLeft: item.left, elemTop: item.top,
			elemWidth: item.width, elemHeight: item.height, date: new Date()
		});
	});

	return updatedChildren;
};

/**
  * Returns the movement bounds of a Partition by the minimum and maximum coordinate
	* that each side can move to. This is only applicable when a partition has neighbors
	* while snapped.
  *
  */
Partition.prototype.getMovementBoundaries = function() {
	let partitions = this.partitionList;
	let config = partitions.configuration;
	let titleBar = config.ui.titleBarHeight;

	// bounds to be used to clamp this partitions position/size and then update neighbors
	let partitionMovementBounds = {
		left: {},
		right: {},
		top: {},
		bottom: {}
	};

	// initialized with maximum in every dimension
	// after populating array, take max of min bounds, min of max bounds
	let boundCollections = {
		left: {min: [0], max: [config.totalWidth - partitions.minSize.width]},
		right: {min: [partitions.minSize.width], max: [config.totalWidth]},
		top: {min: [titleBar], max: [config.totalHeight - partitions.minSize.height]},
		bottom: {min: [titleBar + partitions.minSize.height], max: [config.totalHeight - titleBar]}
	};

	// calculate the bounds that the current partition can move
	for (let ptnID of Object.keys(this.neighbors)) {
		let neighPtn = partitions.list[ptnID];
		let neighInfo = this.neighbors[ptnID];

		// if the top of this partition is shared with the bottom of another
		if (neighInfo.top) {

			// check which of the 2 sides is the same
			if (neighInfo.top === "bottom") {
				boundCollections.top.min.push(neighPtn.top + partitions.minSize.height + titleBar);
			} else { // "top"
				boundCollections.top.max.push(neighPtn.top + neighPtn.height - partitions.minSize.height);
			}
		}
		// if the bottom of this partition is shared with the top of another
		if (neighInfo.bottom) {
			if (neighInfo.bottom === "top") {
				boundCollections.bottom.max.push(neighPtn.top + neighPtn.height - partitions.minSize.height - titleBar);
			} else { // "bottom"
				boundCollections.bottom.min.push(neighPtn.top + partitions.minSize.height);
			}
		}
		// if the left of this partition is shared with the right of another
		if (neighInfo.left) {
			if (neighInfo.left === "right") {
				boundCollections.left.min.push(neighPtn.left + partitions.minSize.width);
			} else { // "left"
				boundCollections.left.max.push(neighPtn.left + neighPtn.width - partitions.minSize.width);
			}
		}
		// if the right of this partition is shared with the left of another
		if (neighInfo.right) {
			if (neighInfo.right === "left") {
				boundCollections.right.max.push(neighPtn.left + neighPtn.width - partitions.minSize.width);
			} else { // "right"
				boundCollections.right.min.push(neighPtn.left + partitions.minSize.width);
			}
		}
	}

	// calculate take max of min bounds, min of max bounds of each bound collection
	for (let side of Object.keys(boundCollections)) {
		partitionMovementBounds[side].min = Math.max(...boundCollections[side].min);
		partitionMovementBounds[side].max = Math.min(...boundCollections[side].max);
	}

	return partitionMovementBounds;
};

/**
  * Clamps each side of the Partition to be within each's respective boundary.
  *
	* @param {object} boundaries - The movement boundaries of a Partition (from Partition.getMovementBoundaries)
  */
Partition.prototype.clampPositionWithinBoundaries = function (boundaries) {
	let partitions = this.partitionList;
	let config = partitions.configuration;
	let titleBar = config.ui.titleBarHeight;

	// clamp this partition within the bounds
	let newPositionAfterClamp = {
		left: this.left,
		right: this.left + this.width,
		top: this.top,
		bottom: this.top + this.height
	};

	// clamp left
	if (this.left < boundaries.left.min) {
		newPositionAfterClamp.left = boundaries.left.min;
	} else if (this.left > boundaries.left.max) {
		newPositionAfterClamp.left = boundaries.left.max;
	}

	// clamp top
	if (this.top < boundaries.top.min) {
		newPositionAfterClamp.top = boundaries.top.min;
	} else if (this.top > boundaries.top.max) {
		newPositionAfterClamp.top = boundaries.top.max;
	}

	// clamp right
	if (this.left + this.width < boundaries.right.min) {
		newPositionAfterClamp.right = boundaries.right.min;
	} else if (this.left + this.width > boundaries.right.max) {
		newPositionAfterClamp.right = boundaries.right.max;
	}

	// clamp bottom
	if (this.top + this.height < boundaries.bottom.min) {
		newPositionAfterClamp.bottom = boundaries.bottom.min;
	} else if (this.top + this.height > boundaries.bottom.max) {
		newPositionAfterClamp.bottom = boundaries.bottom.max;
	}

	// check for screen boundary snapping
	if (this.snapTop) {
		newPositionAfterClamp.top = titleBar;
	}
	if (this.snapLeft) {
		newPositionAfterClamp.left = 0;
	}
	if (this.snapRight) {
		newPositionAfterClamp.right = config.totalWidth;
	}
	if (this.snapBottom) {
		newPositionAfterClamp.bottom = config.totalHeight - titleBar;
	}

	this.left = newPositionAfterClamp.left;
	this.top = newPositionAfterClamp.top;
	this.width = newPositionAfterClamp.right - this.left;
	this.height = newPositionAfterClamp.bottom - this.top;
};

/**
  * Updates the position of all of the Partitions neighbors based on the position of the Partition
	* after it has been clamped into its boundaries.
  */
Partition.prototype.updateNeighborPtnPositions = function() {
	let partitions = this.partitionList;
	let config = partitions.configuration;
	let titleBar = config.ui.titleBarHeight;

	// bounds to be used to clamp this partitions position/size and then update neighbors
	let partitionMovementBounds = this.getMovementBoundaries();

	// clamp with those bounds
	this.clampPositionWithinBoundaries(partitionMovementBounds);

	// update all neighbors based on this position of the partition

	let updatedPtnIDs = [];

	for (var neigh of Object.keys(this.neighbors)) {

		// make sure this partition is in partitions (list)
		if (partitions.list.hasOwnProperty(neigh)) {
			let neighPtn = partitions.list[neigh];
			let isUpdated = false;

			// if the top of this partition is shared with the bottom of another
			if (this.neighbors[neigh].top) {

				// check which of the 2 sides is the same
				if (this.neighbors[neigh].top === "bottom") {
					// adjust height of neighbor
					neighPtn.height = this.top - neighPtn.top - titleBar;

					isUpdated = true;
				} else { // "top"
					// save bottom coordinate
					let botCoord = neighPtn.top + neighPtn.height;

					// adjust the top and height of neighbor
					neighPtn.top = this.top;
					neighPtn.height = botCoord - neighPtn.top;

					isUpdated = true;
				}
			}
			// if the bottom of this partition is shared with the top of another
			if (this.neighbors[neigh].bottom) {
				if (this.neighbors[neigh].bottom === "top") {
					// save bottom coordinate
					let botCoord = neighPtn.top + neighPtn.height;

					// adjust the top and height of neighbor
					neighPtn.top = this.top + this.height + titleBar;
					neighPtn.height = botCoord - neighPtn.top;

					isUpdated = true;
				} else { // "bottom"
					// adjust height of neighbor
					neighPtn.height = this.top + this.height - neighPtn.top;

					isUpdated = true;
				}
			}
			// if the left of this partition is shared with the right of another
			if (this.neighbors[neigh].left) {
				if (this.neighbors[neigh].left === "right") {
					// adjust width of neighbor
					neighPtn.width = this.left - neighPtn.left;

					isUpdated = true;
				} else { // "left"
					// save right coordinate
					let rightCoord = neighPtn.left + neighPtn.width;

					// adjust the left and width of neighbor
					neighPtn.left = this.left;
					neighPtn.width = rightCoord - neighPtn.left;

					isUpdated = true;
				}
			}
			// if the right of this partition is shared with the left of another
			if (this.neighbors[neigh].right) {
				if (this.neighbors[neigh].right === "left") {
					// save right coordinate
					let rightCoord = neighPtn.left + neighPtn.width;

					// adjust the left and width of neighbor
					neighPtn.left = this.left + this.width;
					neighPtn.width = rightCoord - neighPtn.left;

					isUpdated = true;
				} else { // "right"
					// adjust width of neighbor
					neighPtn.width = this.left + this.width - neighPtn.left;

					isUpdated = true;
				}
			}

			if (isUpdated) {
				updatedPtnIDs.push(neigh);
			}
		}
	}

	// return the IDs of updated Partitions so the updates can be reflected in
	// ui and display
	return updatedPtnIDs;
};

/**
  * Toggles whether or not the Partition is being snapped to its neighbors.
  */
Partition.prototype.toggleSnapping = function() {
	this.isSnapping = !this.isSnapping;

	return this.updateNeighborPartitionList();
};

/**
  * Updates the list of neighbors and which side they are neighboring on for this
	* Partition
  */
Partition.prototype.updateNeighborPartitionList = function() {
	return this.partitionList.updateNeighbors(this.id);
};

/**
  * Get a string corresponding to the information needed to update the display
  */
Partition.prototype.getDisplayInfo = function() {
	return {
		id: this.id,
		color: this.color,

		left: this.left,
		top: this.top,
		width: this.width,
		height: this.height,

		// snapped to other partitions
		snapping: !this.isSnapping || !this.neighbors ? {left: false, right: false, top: false, bottom: false} : {
			left: Object.values(this.neighbors).reduce((snapped, neighbor) => snapped || neighbor.left, false),
			right: Object.values(this.neighbors).reduce((snapped, neighbor) => snapped || neighbor.right, false),
			top: Object.values(this.neighbors).reduce((snapped, neighbor) => snapped || neighbor.top, false),
			bottom: Object.values(this.neighbors).reduce((snapped, neighbor) => snapped || neighbor.bottom, false)
		},

		// anchored to sides of screen
		anchor: {
			left: this.snapLeft,
			right: this.snapRight,
			top: this.snapTop,
			bottom: this.snapBottom
		}
	};
};

/**
  * Get a string corresponding to the information needed to update the display
  */
Partition.prototype.getTitle = function() {
	var partitionString = "";
	if (this.numChildren === 0) {
		partitionString = "Empty";
	} else if (this.numChildren === 1) {
		partitionString = "1 Item";
	} else {
		partitionString = this.numChildren + " Items";
	}
	if (this.currentMaximizedChild && this.innerTiling) {
		partitionString += " | Maximized & Tiled";
	} else if (this.innerTiling) {
		partitionString += " | Tiled";
	}

	return {
		id: this.id,
		title: partitionString
	};
};

/**
  * Create the context menu based on the current state of the Partition
  */
Partition.prototype.getContextMenu = function() {
	var contextMenu = [];

	contextMenu.push({
		description: "Content Management",
		parameters: {},
		children: [
			{
				description: "Clear",
				callback: "clearPartition",
				parameters: {}
			},
			{
				description: this.innerTiling ? "Stop Tiling" : "Tile",
				callback: "toggleInnerTiling",
				parameters: {}
			}
		]
	});

	contextMenu.push({
		description: "Partition Snapping",
		callback: "toggleSnapping",
		parameters: {},
		children: this.isSnapping ?
			[
				{
					description: "Un-Snap",
					callback: "toggleSnapping",
					parameters: {}
				},
				{
					description: "Update Neighbors",
					callback: "updateNeighborPartitionList",
					parameters: {}
				}
			] : [
				{
					description: "Snap",
					callback: "toggleSnapping",
					parameters: {}
				}
			]
	});

	contextMenu.push({
		description: "separator"
	});

	contextMenu.push({
		description: "Set Color: ",
		callback: "setColor",
		value: this.color,
		inputField: true,
		// color input field (special input)
		inputType: "color",
		colorChoices: [
			'#a6cee3',
			'#1f78b4',
			'#b2df8a',
			'#33a02c',
			'#fb9a99',
			'#e31a1c',
			'#fdbf6f',
			'#ff7f00',
			'#cab2d6',
			'#6a3d9a',
			'#ffff99',
			'#b15928'
		],
		inputFieldSize: 7,
		inputDefault: this.color,
		inputUpdateOnChange: true,
		parameters: {}
	});

	contextMenu.push({
		description: this.maximized ? "Restore Partition" : "Maximize Partition",
		callback: "SAGE2Maximize",
		parameters: {}
	});
	contextMenu.push({
		description: "Close Partition",
		callback: "SAGE2DeleteElement",
		parameters: {}
	});

	return contextMenu;
};

Partition.prototype.print = function(data) {
	console.log(data);

	this.sliderVal = data.clientInput;
	console.log(this.sliderVal);
};


module.exports = Partition;