API Docs for: 2.0.0

public/src/pdf_viewer.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) 2016

/* global d3 */

"use strict";

/**
 * @module client
 * @submodule pdf_viewer
 */

PDFJS.workerSrc       = 'lib/pdf.worker.js';
PDFJS.disableWorker   = false;
PDFJS.disableWebGL    = true;
PDFJS.verbosity       = PDFJS.VERBOSITY_LEVELS.warnings;
PDFJS.maxCanvasPixels = 67108864; // 8k2
PDFJS.disableStream   = true;

// List of icons
var svgImages = [
	'arrowLeftBtnOff.svg',   // 0
	'arrowLeftBtnOn.svg',    // 1
	'arrowRightBtnOn.svg',   // 2
	'arrowRightBtnOff.svg',  // 3
	'addPage.svg',           // 4
	'deletePage.svg',        // 5
	'thumbnail.svg'];        // 6

// Folder containing the icons
var iconPath = "/images/appUi/";

/**
 * PDF viewing application, based on pdf.js library
 *
 * @class pdf_viewer
 */
var pdf_viewer = SAGE2_App.extend({

	modifyState: function(name, value) {
		this.state[name] = value;
		this.SAGE2Sync(true);
	},

	/**
	* Init method, creates an 'img' tag in the DOM and a few canvas contexts to handle multiple redraws
	*
	* @method init
	* @param data {Object} contains initialization values (id, width, height, ...)
	*/
	init: function(data) {
		// Create div into the DOM
		this.SAGE2Init("div", data);

		// Set the background
		this.element.style.backgroundColor = '#272822';

		// move and resize callbacks
		this.resizeEvents = "continuous";

		// SAGE2 Application Settings
		//
		// Control the frame rate for an animation application
		this.maxFPS = 2.0;

		this.activeTouch    = [];
		this.interactable   = [];
		this.gotInformation = false;
		this.pageDocument   = 0;
		this.baseWidthPage  = null;
		this.baseHeightPage = null;
		this.pageCurrentlyVisible  = {};
		this.pageCurrentlyGenerated = {};
		this.loaded = false;
		this.TVALUE = 0.25;
		this.showUI = true;
		this.title  = data.title;

		// disable gap between pages (bug in scaling)
		// this.displacement = this.marginButton;
		this.displacement = 0;

		this.marginButton    = 5;
		this.thumbnailHeight = 0;
		this.thumbnailHorizontalPosition = 0;
		this.resizeValue = 1;
		this.previousResizeValue = 1;

		// svg container, big as the application
		this.container = d3.select(this.element).append("svg").attr("id", "container");
		this.container
			.style("position", "absolute")
			.style("left", 0)
			.attr("width",  this.element.clientWidth)
			.attr("height", this.element.clientHeight);

		// the group that will visualize the images
		this.imageVisualizer = this.container.append("g");
		this.imageVisualizer.groups = {};
		for (var i = 1; i <= 3; i++) {
			this.imageVisualizer.groups[i] = this.imageVisualizer.append("g").style("visibility", "hidden");
		}

		// this.imageVisualizer = this.container.append("g");
		// array used to store the svg image item used to visualize the images
		this.imageViewers = {};
		// array containing the image links
		this.imagesLink = {};

		// the group that will visualize the thumbnails
		this.thumbnailsVisualizer = this.container.append("g");
		// array used to store the svg image for thumbnails
		this.thumbnailsViewers = [];
		// array containing the image links
		this.thumbnailsLink = [];

		// menu bar
		this.commandBarG = this.container.append("g");

		this.imageVisualizer.groups[Math.ceil(this.resizeValue)] =
			this.imageVisualizer.append("g").style("visibility", "visible");
		this.thumbnailsVisualizer.style("visibility",
			this.state.showingThumbnails ? "visible" : "hidden");

		// if no state available, load new pdf
		this.loadPDF(cleanURL(this.state.doc_url));
	},

	loadPDF: function(docURL) {
		var _this = this;
		PDFJS.getDocument(docURL).then(function(solver) {

			// saving the solver
			_this.solver = solver;

			// memorize the number of page of the document
			_this.pageDocument = solver.numPages;

			// no UI needed when only one page
			if (_this.pageDocument === 1) {
				_this.showUI = false;
			}

			// load the first page the get the INFORMATION
			_this.obtainPageFromPDF(solver, 1, _this, 1);

			// generating the thumbnails - i do not need to generate thumbnails.
			for (var i = 1; i <= _this.pageDocument; i++) {
				_this.obtainPageFromPDF(solver, i, _this, _this.TVALUE);
			}

			// Update the title
			_this.changeTitle();

			// Build the wall UI
			_this.addWidgetControlsToPdfViewer();
		});
	},

	/**
	* Adds custom widgets to app
	*
	* @method addWidgetControlsToPdfViewer
	*/
	addWidgetControlsToPdfViewer: function() {
		if (this.pageDocument > 1) {
			this.controls.addButton({type: "fastforward", position: 6, identifier: "LastPage"});
			this.controls.addButton({type: "rewind",      position: 2, identifier: "FirstPage"});
			this.controls.addButton({type: "prev",        position: 3, identifier: "PreviousPage"});
			this.controls.addButton({type: "next",        position: 5, identifier: "NextPage"});
			this.controls.addSlider({
				minimum: 1,
				maximum: this.pageDocument,
				increments: 1,
				property: "this.state.currentPage",
				label: "Page",
				identifier: "Page"
			});
		}
		this.controls.finishedAddingControls();
	},

	/**
	 * Update the tile with current page number
	 *
	 * @method     `eTitle
	 */
	changeTitle: function() {
		// Get the page in center of the screen
		var currentNumber = this.pageInCenter();
		// Boundaries check
		currentNumber = (currentNumber < 1) ? 1 : currentNumber;
		currentNumber = (currentNumber > this.pageDocument) ? this.pageDocument : currentNumber;
		var newTitle  = this.title + " (page " + currentNumber + " of " + this.pageDocument + ")";
		this.updateTitle(newTitle);
	},

	obtainPageFromPDF: function(pdfFile, pageNumber, that, quality) {

		var thumbnail = (quality === that.TVALUE) ? true : false;

		pdfFile.getPage(pageNumber).then(function(page) {

			var canvas = document.createElement("canvas");
			var ctx = canvas.getContext("2d");
			var viewport;
			viewport = page.getViewport(quality);

			if (!that.gotInformation && !thumbnail) {
				if (that.baseWidthPage == null) {
					that.baseWidthPage  = viewport.width;
					that.baseHeightPage = viewport.height;
				}
				that.gotInformation = true;

				that.createMenuBar();

				var dx = (-1) * that.baseWidthPage * (that.state.currentPage - 1);
				that.imageVisualizer.attr("transform", "translate(" + dx + ",0)");

				that.scaleThumbnailBar();

				var commandDy = that.state.showingThumbnails ? that.thumbnailHeight * that.resizeValue : 0;
				that.commandBarG.attr("transform", "translate(0," + (viewport.height + commandDy) + ")");

				that.loaded = true;

				var neww = that.baseWidthPage * that.state.numberOfPageToShow * that.resizeValue;
				var newh = (that.baseHeightPage + that.commandBarG.height + that.thumbnailHeight) * that.resizeValue;
				// calculate the aspect ratio
				var newar = neww / newh;
				// use the same width as specified by the server
				neww = that.sage2_width;
				// adjust the height
				newh = neww / newar;
				// ask for a size update
				that.sendResize(neww, newh);
				return;
			}

			canvas.width  = viewport.width;
			canvas.height = viewport.height;

			// rendering the page
			var renderContext = {
				canvasContext: ctx,
				viewport:      viewport
			};

			page.render(renderContext).then(function(pdf) {
				// Render as a JPEG, 80%
				// var data = canvas.toDataURL("image/jpeg", 0.80).split(',');
				// Render as a PNG
				var data = canvas.toDataURL().split(',');

				var bin  = atob(data[1]);
				var mime = data[0].split(':')[1].split(';')[0];
				var buf  = new ArrayBuffer(bin.length);
				var view = new Uint8Array(buf);
				for (var i = 0; i < view.length; i++) {
					view[i] = bin.charCodeAt(i);
				}
				var blob = new Blob([buf], {type: mime});
				var source = window.URL.createObjectURL(blob);

				var theWidth, theHeight, dx, c;

				if (thumbnail) {

					that.thumbnailsLink[page.pageNumber] = source;

					theWidth  = that.baseWidthPage  * that.TVALUE;
					theHeight = that.baseHeightPage * that.TVALUE;
					dx = (theWidth + that.marginButton * that.TVALUE) * (page.pageNumber - 1);

					c = that.thumbnailsVisualizer.append("image")
						.attr("x", dx)
						.attr("y", 0)
						.attr("width",  theWidth)
						.attr("height", theHeight + 2 * that.marginButton)
						.attr("xlink:href", source);

					c.thumbnail = true;
					c.container = that.thumbnailsVisualizer;
					c.page = page.pageNumber;
					that.interactable.push(c);
					that.thumbnailsViewers.push(c);

				} else {
					if (!that.imagesLink[quality]) {
						that.imagesLink[quality] = [];
					}
					that.imagesLink[quality][page.pageNumber] = source;

					theWidth  = that.baseWidthPage  * quality;
					theHeight = that.baseHeightPage * quality;
					dx = (theWidth + that.displacement) * (page.pageNumber - 1);

					c = that.imageVisualizer.groups[quality].append("image")
						.attr("x", dx)
						.attr("y", 0)
						.attr("width", theWidth)
						.attr("height", theHeight)
						.attr("xlink:href", source);
				}
			});
		});
	},

	changeImageQuality: function(q, previousQ) {
		this.imageVisualizer.groups[previousQ].style("visibility", "hidden");
		if (!this.imageVisualizer.groups[q]) {
			this.imageVisualizer.groups[q] = this.imageVisualizer.append("g").style("visibility", "visible");
		} else {
			this.imageVisualizer.groups[q].style("visibility", "visible");
		}
	},

	resize: function(date) {
		if (!this.loaded) {
			return;
		}

		var r = this.element.clientWidth / this.state.numberOfPageToShow / Math.round(this.baseWidthPage);
		this.resizeValue = r;

		var qualityRequested = Math.ceil(this.resizeValue);
		var previousQualityRequested = Math.ceil(this.previousResizeValue);

		var scale = r / qualityRequested;

		this.translateGroup(this.imageVisualizer, this.state.horizontalOffset, 0, scale);
		this.translateGroup(this.thumbnailsVisualizer, this.thumbnailHorizontalPosition,
			this.baseHeightPage * r, r, this.clickedThumbnail);
		this.translateGroup(this.commandBarG, null, (this.baseHeightPage + this.thumbnailHeight) * r,
			r, this.clickedThumbnail);

		if (this.clickedThumbnail) {
			this.clickedThumbnail = false;
		}

		if (Math.ceil(this.previousResizeValue) != qualityRequested) {
			this.previousResizeValue = r;
			this.changeImageQuality(qualityRequested, previousQualityRequested);
		}

		this.generateMissingPages();

		this.container
			.attr("width",  this.element.clientWidth)
			.attr("height", this.element.clientHeight);
		this.refresh(date);
	},

	translateGroup: function(g, dx, dy, s, animated) {
		var transf = parse_transform(g.attr("transform"));
		var scale  = transf.scale ? parseFloat(transf.scale[0]) : 1;
		dx = (dx == null) ? parseFloat(transf.translate[0]) : dx;
		dy = (dy == null) ? parseFloat(transf.translate[1]) : dy;
		s  = (s  == null) ? scale : s;
		var tDuration = animated ? 200 : 0;
		g.transition().attr("transform",
			"translate(" + dx * this.resizeValue + "," + dy +
			"), scale(" + s + ")").duration(tDuration);
	},

	generateMissingPages: function() {

		var q = Math.ceil(this.resizeValue);

		// generating array of images of current quality, if not available
		if (!this.pageCurrentlyVisible[q]) {
			this.pageCurrentlyVisible[q] = [];
			this.SAGE2Sync(true);
		}
		if (!this.pageCurrentlyGenerated[q]) {
			this.pageCurrentlyGenerated[q] = [];
			this.SAGE2Sync(true);
		}

		// calculate page in view
		var halfRange = Math.floor(this.state.numberOfPageToShow / 2);
		for (var i = this.pageInCenter() - halfRange - 1; i <= this.pageInCenter() + halfRange + 1; i++) {
			if (i > 0 && i <= this.pageDocument && this.pageCurrentlyVisible[q].indexOf(i) === -1) {
				this.pageCurrentlyVisible[q].push(i);
				this.SAGE2Sync(true);
			}
		}

		// generate page not already loaded
		for (var index in this.pageCurrentlyVisible[q]) {
			var pageIndex = this.pageCurrentlyVisible[q][index];
			if (this.pageCurrentlyGenerated[q].indexOf(pageIndex) === -1) {
				this.pageCurrentlyGenerated[q].push(pageIndex);
				this.SAGE2Sync(true);
				this.obtainPageFromPDF(this.solver, pageIndex, this, q);
			}
		}

	},

	leftClickDown: function(x, y, id) {
		// setting the feedback button color
		var pressedColor = "gray";
		var defaultBg    = "lightgray";

		// taking a reference of the main object
		var _this = this;
		// iterating over the model trying to understand if a button was pressed
		for (var i in this.interactable) {
			var item = this.interactable[i];
			var position = {
				x: parseInt(item.attr("x")),
				y: parseInt(item.attr("y")),
				w: parseInt(item.attr("width")),
				h: parseInt(item.attr("height")),
				container: item.container
			};
			// check if the click is within the current button
			if (item.command && within(position, x, y)) {
				// if the button is clickable, generates a color transition feedback
				if (item.command) {
					item.action(_this);
					// feedback
					var oldColor = item.backgroundColor || defaultBg;
					item.transition();
					item.attr("fill", pressedColor).transition().duration(500).attr("fill", oldColor);
				}
				return;
			}

			if (item.thumbnail && within(position, x, y)) {

				if (item.clickReceived) {
					this.doubleLeftClickPosition(x, y, id, item);
					item.clickReceived = null;
					return;
				} else {
					item.clickReceived = true;
					setTimeout(deleteClick, 500, item);
				}

				this.activeTouch[id] = {};
				this.activeTouch[id].lastMousePosition = {x: x, y: y};
				this.activeTouch[id].item = item;
			}

		}

	},

	leftClickMove: function(x, y, id) {
		var position = {
			x: parseInt(this.commandBarBG.attr("x")),
			y: parseInt(this.commandBarBG.attr("y")),
			w: parseInt(this.commandBarBG.attr("width")),
			h: parseInt(this.commandBarBG.attr("height")),
			container: this.commandBarBG.container
		};

		// check if the click is within the current button
		if (this.inBarCommand == null && within(position, x, y)) {
			// Do not move the widget bar at the bottom anymore
			// var center = ((this.widthCommandButton + this.marginButton) *
			// 	(this.commandBarG.node().childNodes.length - 1) / 2) / 2;
			// var iFound = 0;
			// for (var i in this.interactable) {
			// 	var item = this.interactable[i];
			// 	if (item.ico) {
			// 		item.transition().attr("x", x / this.resizeValue +
			// 			(this.widthCommandButton + this.marginButton) * iFound - center).duration(200);
			// 		item.ico.transition().attr("x", x / this.resizeValue +
			// 			(this.widthCommandButton + this.marginButton) * iFound - center).duration(200);
			// 		iFound += 1;
			// 	}
			// }
			this.inBarCommand = true;
		} else if (!within(position, x, y)) {
			this.inBarCommand = null;
		}

		var f = this.activeTouch[id];
		if (f && this.state.showingThumbnails && f.item.thumbnail) {
			var transf = parse_transform(f.item.container.attr("transform"));
			var sx = parseFloat(transf.scale[0]);
			var translate = transf.translate;
			var newX = parseFloat(translate[0]) + x - f.lastMousePosition.x;
			var newY = parseFloat(translate[1]);
			newX /= sx;
			newY /= sx;
			f.lastMousePosition = {x: x, y: y};
			this.thumbnailHorizontalPosition = newX;
			this.thumbnailsVisualizer.attr("transform", "scale(" + sx + "), translate(" + newX + "," + newY + ")");
		}
	},

	leftClickRelease: function(x, y, id) {
		var f = this.activeTouch[id];
		if (f) {
			// empty
		}
		delete this.activeTouch[id];
	},

	doubleLeftClickPosition: function(x, y, id, item) {
		if (this.state.showingThumbnails) {
			this.goToPage(item.page);
		}
	},

	pageInCenter: function() {
		return Math.floor((this.state.horizontalOffset * (-1) * this.resizeValue +
			this.element.clientWidth / 2) / (this.baseWidthPage * this.resizeValue) + 1);
	},

	addPage: function(that) {
		if (that.state.numberOfPageToShow < that.pageDocument) {
			that.modifyState("numberOfPageToShow", that.state.numberOfPageToShow + 1);
			var neww = that.baseWidthPage * that.state.numberOfPageToShow * that.resizeValue;
			var newh = (that.baseHeightPage + that.commandBarG.height + that.thumbnailHeight) * that.resizeValue;
			that.sendResize(neww, newh);
		}
	},

	removePage: function(that) {
		if (that.state.numberOfPageToShow > 1) {
			that.modifyState("numberOfPageToShow", that.state.numberOfPageToShow - 1);
			var neww = that.baseWidthPage * that.state.numberOfPageToShow * that.resizeValue;
			var newh = (that.baseHeightPage + that.commandBarG.height + that.thumbnailHeight) * that.resizeValue;
			that.sendResize(neww, newh);
		}
	},

	/**
	* Callback from right-click menu for adding/removing pages
	*
	* @method pageCallback
	* @param responseObject {Object} contains operation to perform
	*/
	pageCallback: function(responseObject) {
		if (responseObject.operation === 'add') {
			this.addPage(this);
		} else if (responseObject.operation === 'remove') {
			this.removePage(this);
		}
	},

	showThumbnails: function(that) {
		that.modifyState("showingThumbnails", !that.state.showingThumbnails);
		that.thumbnailsVisualizer.style("visibility", that.state.showingThumbnails ? "visible" : "hidden");
		that.clickedThumbnail = true;

		var multiplier = that.state.showingThumbnails ? 0.25 : 0;
		that.thumbnailHeight = that.baseHeightPage * multiplier;

		var neww = that.baseWidthPage * that.state.numberOfPageToShow * that.resizeValue;
		var newh = (that.baseHeightPage + that.commandBarG.height + that.thumbnailHeight) * that.resizeValue;
		that.sendResize(neww, newh);
	},

	scaleThumbnailBar: function() {
		var ty = this.baseHeightPage / this.resizeValue;
		this.thumbnailsVisualizer.attr("transform", "scale(" + this.resizeValue +
			"), translate(" + this.thumbnailHorizontalPosition + "," + ty + ")");
	},

	goToPage: function(page) {
		// var center = (this.baseWidthPage / 2) * (this.state.numberOfPageToShow - 1);
		// var dx = center - (this.baseWidthPage + this.displacement) * (page - 1);
		var dx = -1 * (this.baseWidthPage + this.displacement) * (page - 1);
		this.state.horizontalOffset = dx;
		this.generateMissingPages();
		if (this.state.currentPage !== page) {
			this.modifyState("currentPage", page);
		}
		this.translateGroup(this.imageVisualizer, this.state.horizontalOffset, 0);
		return dx;
	},

	GoToNext: function(that) {
		if (that.state.currentPage === that.pageDocument) {
			return;
		}

		if (that.state.currentPage === that.pageDocument - 1) {
			that.nextButton.ico.attr("xlink:href", iconPath + svgImages[3]);
		}
		that.previousButton.ico.attr("xlink:href", iconPath + svgImages[1]);
		that.goToPage(that.state.currentPage + 1);
		that.refresh();
	},

	GoToPrevious: function(that) {
		if (that.state.currentPage === 1) {
			return;
		}

		if (that.state.currentPage === 2) {
			that.previousButton.ico.attr("xlink:href", iconPath + svgImages[0]);
		}
		that.goToPage(that.state.currentPage - 1);
		that.nextButton.ico.attr("xlink:href", iconPath + svgImages[2]);
		that.refresh();
	},

	GoToFirst: function(that) {
		that.goToPage(1);
		that.previousButton.ico.attr("xlink:href", iconPath + svgImages[0]);
		that.nextButton.ico.attr("xlink:href", iconPath + svgImages[2]);
		that.refresh();
	},

	GoToLast: function(that) {
		that.goToPage(that.pageDocument);
		that.previousButton.ico.attr("xlink:href", iconPath + svgImages[1]);
		that.nextButton.ico.attr("xlink:href", iconPath + svgImages[3]);
		that.refresh();
	},

	createMenuBar: function() {

		if (this.commandBarG) {
			this.commandBarG.selectAll("*").remove();
		}

		this.commandBarG.height = this.baseHeightPage * 0.1;
		this.widthCommandButton = this.commandBarG.height - this.marginButton * 2;

		if (!this.showUI) {
			this.commandBarG.height = 0;
			return;
		}

		// the background
		this.commandBarBG = this.commandBarG.append("rect")
			.attr("x", -3000).attr("y", 0)
			.attr("height", this.commandBarG.height)
			.attr("width", 10000)
			.attr("fill", "#272822");
		this.commandBarBG.container = this.commandBarG;

		// the previous < button
		this.previousButton = this.commandBarG.append("rect")
			.attr("x", 0 + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("fill", "lightgray");
		this.previousButton.ico = this.commandBarG.append("image")
			.attr("x",  0 + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("xlink:href", iconPath + svgImages[0]);
		this.previousButton.command = true;
		this.previousButton.action = this.GoToPrevious;
		this.previousButton.container = this.commandBarG;
		this.interactable.push(this.previousButton);

		// the next > button
		this.nextButton = this.commandBarG.append("rect")
			.attr("x", parseInt(this.previousButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("fill", "lightgray");
		this.nextButton.ico = this.commandBarG.append("image")
			.attr("x", parseInt(this.previousButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("xlink:href", iconPath + svgImages[2]);
		this.nextButton.command = true;
		this.nextButton.action = this.GoToNext;
		this.nextButton.container = this.commandBarG;
		this.interactable.push(this.nextButton);

		// the plus button
		this.plusButton = this.commandBarG.append("rect")
			.attr("x", parseInt(this.nextButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("fill", "lightgray");
		this.plusButton.ico = this.commandBarG.append("image")
			.attr("x", parseInt(this.nextButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("xlink:href", iconPath + svgImages[4]);
		this.plusButton.command = true;
		this.plusButton.action = this.addPage;
		this.plusButton.container = this.commandBarG;
		this.interactable.push(this.plusButton);

		// the minus button
		this.minusButton = this.commandBarG.append("rect")
			.attr("x", parseInt(this.plusButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("fill", "lightgray");
		this.minusButton.ico = this.commandBarG.append("image")
			.attr("x", parseInt(this.plusButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("xlink:href", iconPath + svgImages[5]);
		this.minusButton.command = true;
		this.minusButton.action = this.removePage;
		this.minusButton.container = this.commandBarG;
		this.interactable.push(this.minusButton);

		// the show thumbnails button
		this.thumbnailsButton = this.commandBarG.append("rect")
			.attr("x", parseInt(this.minusButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("fill", "lightgray");
		this.thumbnailsButton.ico = this.commandBarG.append("image")
			.attr("x", parseInt(this.minusButton.attr("x")) + this.widthCommandButton + this.marginButton)
			.attr("y", 0 + this.marginButton)
			.attr("width", this.widthCommandButton)
			.attr("height", this.widthCommandButton)
			.attr("xlink:href", iconPath + svgImages[6]);
		this.thumbnailsButton.command = true;
		this.thumbnailsButton.action = this.showThumbnails;
		this.thumbnailsButton.container = this.commandBarG;
		this.interactable.push(this.thumbnailsButton);
	},

	load: function(date) {
		// This check is necessary for remote sharing.
		// Activating generateMissingPages() will infinitely loop on first sync.
		if (!this.hadFirstRemoteLoad) {
			this.hadFirstRemoteLoad = true;
			return;
		}
		// Update the current page
		this.goToPage(this.state.currentPage);

		// Adjust the number of pages
		var neww = this.baseWidthPage * this.state.numberOfPageToShow * this.resizeValue;
		var newh = (this.baseHeightPage + this.commandBarG.height + this.thumbnailHeight) * this.resizeValue;
		this.sendResize(neww, newh);
	},

	draw: function(date) {
		// Update the title
		this.changeTitle();
	},

	/**
	* To enable right click context menu support,
	* this function needs to be present with this format.
	*/
	getContextEntries: function() {
		var entries = [];
		var entry;

		entry = {};
		entry.description = "First Page";
		entry.accelerator = "\u2191";     // up arrow
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "first";
		entries.push(entry);

		entry = {};
		entry.description = "Previous Page";
		entry.accelerator = "\u2190";     // left arrow
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "previous";
		entries.push(entry);

		entry = {};
		entry.description = "Next Page";
		entry.accelerator = "\u2192";     // right arrow
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "next";
		entries.push(entry);

		entry = {};
		entry.description = "Last Page";
		entry.accelerator = "\u2193";     // down arrow
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.parameters.page = "last";
		entries.push(entry);

		entry = {};
		entry.description = "Jump To: ";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.inputField = true;
		entry.inputFieldSize = 3;
		entries.push(entry);

		entry = {};
		entry.description = "Go to page: ";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.inputField = true;
		entry.inputFieldSize = 3;
		entry.voiceEntryOverload = true; // not visible on UI
		entries.push(entry);

		entry = {};
		entry.description = "Jump to page: ";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.inputField = true;
		entry.inputFieldSize = 3;
		entry.voiceEntryOverload = true; // not visible on UI
		entries.push(entry);

		entry = {};
		entry.description = "Show page: ";
		entry.callback = "changeThePage";
		entry.parameters = {};
		entry.inputField = true;
		entry.inputFieldSize = 3;
		entry.voiceEntryOverload = true; // not visible on UI

		entry.description = "separator";
		entries.push(entry);

		entry = {};
		entry.description = "Show Extra Page";
		entry.accelerator = "+";
		entry.callback = "pageCallback";
		entry.parameters = {operation: 'add'};
		entries.push(entry);

		entry = {};
		entry.description = "Remove Extra Page";
		entry.accelerator = "-";
		entry.callback = "pageCallback";
		entry.parameters = {operation: 'remove'};
		entries.push(entry);

		// Special callback: dowload the file
		entries.push({
			description: "Download PDF",
			callback: "SAGE2_download",
			parameters: {
				url: this.state.doc_url
			}
		});
		entries.push({
			description: "Copy URL",
			callback: "SAGE2_copyURL",
			parameters: {
				url: this.state.doc_url
			}
		});

		return entries;
	},

	/**
	* Support function to allow page changing through right mouse context menu.
	*
	* @method changeThePage
	* @param responseObject {Object} contains response from entry selection
	*/
	changeThePage: function(responseObject) {
		var page = responseObject.page;
		// if the user did the input option
		if (responseObject.clientInput) {
			page = parseInt(responseObject.clientInput);
			if (page > 0 && page <= this.pageDocument) {
				if (page === 1) {
					this.GoToFirst(this);
				} else if (page === this.pageDocument) {
					this.GoToLast(this);
				} else {
					this.previousButton.ico.attr("xlink:href", iconPath + svgImages[1]);
					this.nextButton.ico.attr("xlink:href", iconPath + svgImages[2]);
					this.goToPage(page);
				}
			}
		} else {
			// else check for these word options
			if (page === "first") {
				this.GoToFirst(this);
			} else if (page === "previous") {
				if (this.pageInCenter() === 1) {
					return;
				}
				this.GoToPrevious(this);
			} else if (page === "next") {
				if (this.pageInCenter() === this.pageDocument) {
					return;
				}
				this.GoToNext(this);
			} else if (page === "last") {
				this.GoToLast(this);
			}
		}
		// This needs to be a new date for the extra function.
		this.refresh(new Date(responseObject.serverDate));
	},

	/**
	* Handles event processing, arrow keys to navigate, and r to redraw
	*
	* @method event
	* @param eventType {String} the type of event
	* @param position {Object} contains the x and y positions of the event
	* @param user_id {Object} data about the user who triggered the event
	* @param data {Object} object containing extra data about the event,
	* @param date {Date} current time from the server
	*/
	event: function(eventType, position, user, data, date) {
		if (eventType === "pointerPress" && (data.button === "left")) {
			if (this.showUI) {
				this.leftClickDown(position.x, position.y, user.id);
				this.refresh(date);
			}
		} else if (eventType === "pointerMove") {
			if (this.showUI) {
				this.leftClickMove(position.x, position.y, user.id);
				this.refresh(date);
			}
		} else if (eventType === "pointerRelease" && (data.button === "left")) {
			if (this.showUI) {
				this.leftClickRelease(position.x, position.y, user.id);
				this.refresh(date);
			}
		}

		if (eventType === "specialKey") {
			var newOffset, center, minOffset, step;

			if (data.code === 39 && data.state === "down") {
				// Right Arrow

				if (data.status.SHIFT) {
					// calculate a offset amount
					step = (this.baseWidthPage + this.displacement) / 10;
					// apply offset
					newOffset = this.state.horizontalOffset - step;
					center = (this.baseWidthPage / 2) * (this.state.numberOfPageToShow - 1);
					minOffset = center - (this.baseWidthPage + this.displacement) * (this.pageDocument - 1);
					if (newOffset < minOffset) {
						newOffset = minOffset;
					}
					this.modifyState("horizontalOffset", newOffset);
					this.translateGroup(this.imageVisualizer, this.state.horizontalOffset, 0);
					this.generateMissingPages();
				} else {
					this.GoToNext(this);
				}
				this.refresh(date);
			} else if (data.code === 37 && data.state === "down") {
				// Left Arrow

				if (data.status.SHIFT) {
					// calculate a offset amount
					step = (this.baseWidthPage + this.displacement) / 10;
					// apply offset
					newOffset = this.state.horizontalOffset + step;
					center = (this.baseWidthPage / 2) * (this.state.numberOfPageToShow - 1);
					if (newOffset > center) {
						newOffset = center;
					}
					this.modifyState("horizontalOffset", newOffset);
					this.translateGroup(this.imageVisualizer, this.state.horizontalOffset, 0);
					this.generateMissingPages();
				} else {
					this.GoToPrevious(this);
				}
				this.refresh(date);
			} else if (data.code === 38 && data.state === "down") {
				// Up Arrow
				this.GoToFirst(this);
				this.refresh(date);
			} else if (data.code === 40 && data.state === "down") {
				// Down Arrow
				this.GoToLast(this);
				this.refresh(date);
			}
		}

		// Keyboard:
		//   spacebar - next
		//   1/f - first
		//   0/l - last
		if (eventType === "keyboard") {
			if (data.character === " ") {
				this.GoToNext(this);
			} else if (data.character === "1" || data.character === "f") {
				this.GoToFirst(this);
			} else if (data.character === "0" || data.character === "l") {
				this.GoToLast(this);
			} else if (data.character === "+") {
				this.addPage(this);
			} else if (data.character === "-") {
				this.removePage(this);
			}
		}

		if (eventType === "widgetEvent") {
			switch (data.identifier) {
				case "LastPage":
					this.GoToLast(this);
					break;
				case "FirstPage":
					this.GoToFirst(this);
					break;
				case "PreviousPage":
					this.GoToPrevious(this);
					break;
				case "NextPage":
					this.GoToNext(this);
					break;
				case "Page":
					switch (data.action) {
						case "sliderRelease":
							this.goToPage(this.state.currentPage);
							break;
						default:
							return;
					}
					break;
				default:
					return;
			}
			this.refresh(date);
		}

	}
});


// Extra functions

function deleteClick(item) {
	item.clickReceived = null;
}

function parse_transform(a) {
	var b = {};
	for (var i in a = a.match(/(\w+)\(([^,)]+),?([^)]+)?\)/gi)) {
		/* eslint-disable */
		var c = a[i].match(/[\w\.\-]+/g);
		/* eslint-enable */
		b[c.shift()] = c;
	}
	return b;
}

function within(element, x, y) {
	var transf = parse_transform(element.container.attr("transform"));
	var translate = transf.translate;
	var s = transf.scale ? parseFloat(transf.scale[0]) : 1;

	var mX = (x - parseFloat(translate[0]));
	var mY = (y - parseFloat(translate[1]));

	mX /= s;
	mY /= s;

	return (mY >= element.y &&
			mY <= (element.y + element.h) &&
			mX >= element.x &&
			mX <= (element.x + element.w));
}