Просмотр исходного кода

add pdfng module and use it by default

Brian Gough 12 лет назад
Родитель
Сommit
643bda6095

+ 2 - 1
services/web/public/coffee/base.coffee

@@ -11,6 +11,7 @@ define [
 		"underscore"
 		"underscore"
 		"ngSanitize"
 		"ngSanitize"
 		"ipCookie"
 		"ipCookie"
+		"pdfViewerApp"
 	])
 	])
 
 
-	return App
+	return App

+ 1 - 1
services/web/public/coffee/ide.coffee

@@ -6,7 +6,7 @@ define [
 	"ide/online-users/OnlineUsersManager"
 	"ide/online-users/OnlineUsersManager"
 	"ide/track-changes/TrackChangesManager"
 	"ide/track-changes/TrackChangesManager"
 	"ide/permissions/PermissionsManager"
 	"ide/permissions/PermissionsManager"
-	"ide/pdf/PdfManager"
+	"ide/pdfng/PdfManager"
 	"ide/binary-files/BinaryFilesManager"
 	"ide/binary-files/BinaryFilesManager"
 	"ide/settings/index"
 	"ide/settings/index"
 	"ide/share/index"
 	"ide/share/index"

+ 21 - 0
services/web/public/coffee/ide/pdfng/PdfManager.coffee

@@ -0,0 +1,21 @@
+define [
+	"ide/pdfng/controllers/PdfController"
+	"ide/pdfng/controllers/PdfViewToggleController"
+	"ide/pdfng/directives/pdfJs"
+], () ->
+	class PdfManager
+		constructor: (@ide, @$scope) ->
+			@$scope.pdf =
+				url: null # Pdf Url
+				error: false # Server error
+				timeout: false # Server timed out
+				failure: false # PDF failed to compile
+				compiling: false
+				uncompiled: true
+				logEntries: []
+				logEntryAnnotations: {}
+				rawLog: ""
+				view: null # 'pdf' 'logs'
+				showRawLog: false
+				highlights: []
+				position: null

+ 293 - 0
services/web/public/coffee/ide/pdfng/controllers/PdfController.coffee

@@ -0,0 +1,293 @@
+define [
+	"base"
+	"libs/latex-log-parser"
+], (App, LogParser) ->
+	App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking) ->
+		autoCompile = true
+		$scope.$on "project:joined", () ->
+			return if !autoCompile
+			autoCompile = false
+			$scope.recompile(isAutoCompile: true)
+			$scope.hasPremiumCompile = $scope.project.features.compileGroup == "priority"
+
+		sendCompileRequest = (options = {}) ->
+			url = "/project/#{$scope.project_id}/compile"
+			if options.isAutoCompile
+				url += "?auto_compile=true"
+			return $http.post url, {
+				settingsOverride:
+					rootDoc_id: options.rootDocOverride_id or null
+				_csrf: window.csrfToken
+			}
+
+		parseCompileResponse = (response) ->
+			# Reset everything
+			$scope.pdf.error      = false
+			$scope.pdf.timedout   = false
+			$scope.pdf.failure    = false
+			$scope.pdf.uncompiled = false
+			$scope.pdf.url        = null
+
+			if response.status == "timedout"
+				$scope.pdf.timedout = true
+			else if response.status == "autocompile-backoff"
+				$scope.pdf.uncompiled = true
+			else if response.status == "failure"
+				$scope.pdf.failure = true
+				fetchLogs()
+			else if response.status == "success"
+				$scope.pdf.url = "/project/#{$scope.project_id}/output/output.pdf?cache_bust=#{Date.now()}"
+				fetchLogs()
+
+			IGNORE_FILES = ["output.fls", "output.fdb_latexmk"]
+			$scope.pdf.outputFiles = []
+			for file in response.outputFiles
+				if IGNORE_FILES.indexOf(file.path) == -1
+					# Turn 'output.blg' into 'blg file'.
+					if file.path.match(/^output\./)
+						file.name = "#{file.path.replace(/^output\./, "")} file"
+					else
+						file.name = file.path
+					$scope.pdf.outputFiles.push file
+
+		fetchLogs = () ->
+			$http.get "/project/#{$scope.project_id}/output/output.log"
+				.success (log) ->
+					$scope.pdf.rawLog = log
+					logEntries = LogParser.parse(log, ignoreDuplicates: true)
+					$scope.pdf.logEntries = logEntries
+					$scope.pdf.logEntries.all = logEntries.errors.concat(logEntries.warnings).concat(logEntries.typesetting)
+
+					$scope.pdf.logEntryAnnotations = {}
+					for entry in logEntries.all
+						entry.file = normalizeFilePath(entry.file)
+
+						entity = ide.fileTreeManager.findEntityByPath(entry.file)
+						if entity?
+							$scope.pdf.logEntryAnnotations[entity.id] ||= []
+							$scope.pdf.logEntryAnnotations[entity.id].push {
+								row: entry.line - 1
+								type: if entry.level == "error" then "error" else "warning"
+								text: entry.message
+							}
+
+				.error () ->
+					$scope.pdf.logEntries = []
+					$scope.pdf.rawLog = ""
+
+		getRootDocOverride_id = () ->
+			doc = ide.editorManager.getCurrentDocValue()
+			return null if !doc?
+			for line in doc.split("\n")
+				match = line.match /(.*)\\documentclass/
+				if match and !match[1].match /%/
+					return ide.editorManager.getCurrentDocId()
+			return null
+
+		normalizeFilePath = (path) ->
+			path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
+			path = path.replace(/^\/compile\//, "")
+
+			rootDocDirname = ide.fileTreeManager.getRootDocDirname()
+			if rootDocDirname?
+				path = path.replace(/^\.\//, rootDocDirname + "/")
+
+			return path
+
+		compileCount = 0
+		$scope.recompile = (options = {}) ->
+			return if $scope.pdf.compiling
+			$scope.pdf.compiling = true
+			
+			if !options.isAutoCompile
+				compileCount++
+				if compileCount == 1
+					event_tracking.send('editor-interaction', 'single-compile')
+				else if compileCount == 3
+					event_tracking.send('editor-interaction', 'multi-compile')
+
+			options.rootDocOverride_id = getRootDocOverride_id()
+
+			sendCompileRequest(options)
+				.success (data) ->
+					$scope.pdf.view = "pdf"
+					$scope.pdf.compiling = false
+					parseCompileResponse(data)
+				.error () ->
+					$scope.pdf.compiling = false
+					$scope.pdf.error = true
+					
+		# This needs to be public.
+		ide.$scope.recompile = $scope.recompile
+
+		$scope.clearCache = () ->
+			$http {
+				url: "/project/#{$scope.project_id}/output"
+				method: "DELETE"
+				headers:
+					"X-Csrf-Token": window.csrfToken
+			}
+
+		$scope.toggleLogs = () ->
+			if !$scope.pdf.view? or $scope.pdf.view == "pdf"
+				$scope.pdf.view = "logs"
+			else
+				$scope.pdf.view = "pdf"
+
+		$scope.showPdf = () ->
+			$scope.pdf.view = "pdf"
+
+		$scope.toggleRawLog = () ->
+			$scope.pdf.showRawLog = !$scope.pdf.showRawLog
+
+		$scope.openOutputFile = (file) ->
+			window.open("/project/#{$scope.project_id}/output/#{file.path}")
+
+		$scope.openClearCacheModal = () ->
+			modalInstance = $modal.open(
+				templateUrl: "clearCacheModalTemplate"
+				controller: "ClearCacheModalController"
+				scope: $scope
+			)
+
+		$scope.syncToCode = (position) ->
+			synctex
+				.syncToCode(position)
+				.then (data) ->
+					{doc, line} = data
+					ide.editorManager.openDoc(doc, gotoLine: line)
+					
+		$scope.switchToFlatLayout = () ->
+			$scope.ui.pdfLayout = 'flat'
+			$scope.ui.view = 'pdf'
+			$.localStorage "pdf.layout", "flat"
+			
+		$scope.switchToSideBySideLayout = () ->
+			$scope.ui.pdfLayout = 'sideBySide'
+			$scope.ui.view = 'editor'
+			$.localStorage "pdf.layout", "split"
+			
+		if pdfLayout = $.localStorage("pdf.layout")
+			$scope.switchToSideBySideLayout() if pdfLayout == "split"
+			$scope.switchToFlatLayout() if pdfLayout == "flat"
+		else
+			$scope.switchToSideBySideLayout()
+
+	App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
+		synctex =
+			syncToPdf: (cursorPosition) ->
+				deferred = $q.defer()
+
+				doc_id = ide.editorManager.getCurrentDocId()
+				if !doc_id?
+					deferred.reject()
+					return deferred.promise
+				doc = ide.fileTreeManager.findEntityById(doc_id)
+				if !doc?
+					deferred.reject()
+					return deferred.promise
+				path = ide.fileTreeManager.getEntityPath(doc)
+				if !path?
+					deferred.reject()
+					return deferred.promise
+				
+				# If the root file is folder/main.tex, then synctex sees the
+				# path as folder/./main.tex
+				rootDocDirname = ide.fileTreeManager.getRootDocDirname()
+				if rootDocDirname? and rootDocDirname != ""
+					path = path.replace(RegExp("^#{rootDocDirname}"), "#{rootDocDirname}/.")
+
+				{row, column} = cursorPosition
+
+				$http({
+						url: "/project/#{ide.project_id}/sync/code", 
+						method: "GET",
+						params: {
+							file: path
+							line: row + 1
+							column: column
+						}
+					})
+					.success (data) ->
+						deferred.resolve(data.pdf or [])
+					.error (error) ->
+						deferred.reject(error)
+
+				return deferred.promise
+
+			syncToCode: (position, options = {}) ->
+				deferred = $q.defer()
+				if !position?
+					deferred.reject()
+					return deferred.promise
+
+				# It's not clear exactly where we should sync to if it wasn't directly
+				# clicked on, but a little bit down from the very top seems best.
+				if options.includeVisualOffset
+					position.offset.top = position.offset.top + 80
+
+				$http({
+						url: "/project/#{ide.project_id}/sync/pdf", 
+						method: "GET",
+						params: {
+							page: position.page + 1
+							h: position.offset.left.toFixed(2)
+							v: position.offset.top.toFixed(2)
+						}
+					})
+					.success (data) ->
+						if data.code? and data.code.length > 0
+							doc = ide.fileTreeManager.findEntityByPath(data.code[0].file)
+							return if !doc?
+							deferred.resolve({doc: doc, line: data.code[0].line})
+					.error (error) ->
+						deferred.reject(error)
+
+				return deferred.promise
+
+		return synctex
+	]
+
+	App.controller "PdfSynctexController", ["$scope", "synctex", "ide", ($scope, synctex, ide) ->
+		@cursorPosition = null
+		ide.$scope.$on "cursor:editor:update", (event, @cursorPosition) =>
+
+		$scope.syncToPdf = () =>
+			return if !@cursorPosition?
+			synctex
+				.syncToPdf(@cursorPosition)
+				.then (highlights) ->
+					$scope.pdf.highlights = highlights
+
+		$scope.syncToCode = () ->
+			synctex
+				.syncToCode($scope.pdf.position, includeVisualOffset: true)
+				.then (data) ->
+					{doc, line} = data
+					ide.editorManager.openDoc(doc, gotoLine: line)
+	]
+
+	App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
+		$scope.openInEditor = (entry) ->
+			entity = ide.fileTreeManager.findEntityByPath(entry.file)
+			return if !entity? or entity.type != "doc"
+			if entry.line?
+				line = entry.line
+			ide.editorManager.openDoc(entity, gotoLine: line)
+	]
+
+	App.controller 'ClearCacheModalController', ["$scope", "$modalInstance", ($scope, $modalInstance) ->
+		$scope.state =
+			inflight: false
+
+		$scope.clear = () ->
+			$scope.state.inflight = true
+			$scope
+				.clearCache()
+				.then () ->
+					$scope.state.inflight = false
+					$modalInstance.close()
+
+		$scope.cancel = () ->
+			$modalInstance.dismiss('cancel')
+	]

+ 17 - 0
services/web/public/coffee/ide/pdfng/controllers/PdfViewToggleController.coffee

@@ -0,0 +1,17 @@
+define [
+	"base"
+], (App) ->
+	App.controller "PdfViewToggleController", ($scope) ->
+		$scope.togglePdfView = () ->
+			if $scope.ui.view == "pdf"
+				$scope.ui.view = "editor"
+			else
+				$scope.ui.view = "pdf"
+		
+		$scope.fileTreeClosed = false		
+		$scope.$on "layout:main:resize", (e, state) ->
+			if state.west.initClosed
+				$scope.fileTreeClosed = true
+			else
+				$scope.fileTreeClosed = false
+			$scope.$apply()

+ 42 - 0
services/web/public/coffee/ide/pdfng/directives/pdfAnnotations.coffee

@@ -0,0 +1,42 @@
+app = angular.module 'pdfAnnotations', []
+
+app.factory 'pdfAnnotations', [ () ->
+	class pdfAnnotations
+
+		@EXTERNAL_LINK_TARGET = "_blank";
+
+		constructor: (options) ->
+			@annotationsLayerDiv = options.annotations;
+			@viewport = options.viewport
+			@navigateFn = options.navigateFn
+
+		setAnnotations: (annotations) ->
+			for annotation in annotations
+				switch annotation.subtype
+					when 'Link' then @addLink(annotation);
+					when 'Text' then continue
+
+		addLink: (link) ->
+			element = @buildLinkElementFromRect(link.rect);
+			@setLinkTarget(element, link);
+			@annotationsLayerDiv.appendChild(element);
+
+		buildLinkElementFromRect: (rect) ->
+			rect = @viewport.convertToViewportRectangle(rect);
+			rect = PDFJS.Util.normalizeRect(rect);
+			element = document.createElement("a");
+			element.style.left = Math.floor(rect[0]) + 'px';
+			element.style.top = Math.floor(rect[1]) + 'px';
+			element.style.width = Math.ceil(rect[2] - rect[0]) + 'px';
+			element.style.height = Math.ceil(rect[3] - rect[1]) + 'px';
+			element
+
+		setLinkTarget: (element, link) ->
+			if link.url
+				element.href = link.url;
+				element.target = @EXTERNAL_LINK_TARGET;
+			else if (link.dest)
+				element.href = "#" + link.dest;
+				element.onclick = (e) =>
+					@navigateFn link
+]

+ 27 - 0
services/web/public/coffee/ide/pdfng/directives/pdfHighlights.coffee

@@ -0,0 +1,27 @@
+app = angular.module 'pdfHighlights', []
+
+app.factory 'pdfHighlights', [ () ->
+	class pdfHighlights
+
+		constructor: (options) ->
+			@highlightsLayerDiv = options.highlights
+			@viewport = options.viewport
+			@highlightElements = []
+
+		addHighlight: (left, top, width, height) ->
+			rect = @viewport.convertToViewportRectangle([left, top, left + width, top + height])
+			rect = PDFJS.Util.normalizeRect(rect)
+			element = document.createElement("div")
+			element.style.left = Math.floor(rect[0]) + 'px'
+			element.style.top = Math.floor(rect[1]) + 'px'
+			element.style.width = Math.ceil(rect[2] - rect[0]) + 'px'
+			element.style.height = Math.ceil(rect[3] - rect[1]) + 'px'
+			@highlightElements.push(element)
+			@highlightsLayerDiv.appendChild(element)
+			element
+
+		clearHighlights: () ->
+			for h in @highlightElements
+				h.remove()
+			@highlightElements = []
+]

+ 198 - 0
services/web/public/coffee/ide/pdfng/directives/pdfJs.coffee

@@ -0,0 +1,198 @@
+define [
+	"base"
+	"ide/pdfng/directives/pdfViewer"
+	"ide/pdfng/directives/pdfPage"
+	"ide/pdfng/directives/pdfRenderer"
+	"ide/pdfng/directives/pdfTextLayer"
+	"ide/pdfng/directives/pdfAnnotations"
+	"ide/pdfng/directives/pdfHighlights"
+	"libs/pdf"
+	"text!libs/pdfListView/TextLayer.css"
+	"text!libs/pdfListView/AnnotationsLayer.css"
+	"text!libs/pdfListView/HighlightsLayer.css"
+], (
+	App
+	pdfViewerApp
+	pdfPage
+	pdfRenderer
+	pdfTextLayer
+	pdfAnnotations
+	pdfHighlights
+	pdf
+	textLayerCss
+	annotationsLayerCss
+	highlightsLayerCss
+) ->
+	if PDFJS?
+		PDFJS.workerSrc = window.pdfJsWorkerPath
+		PDFJS.disableAutoFetch = true
+
+	style = $("<style/>")
+	style.text(textLayerCss + "\n" + annotationsLayerCss + "\n" + highlightsLayerCss)
+	$("body").append(style)
+
+	App.directive "pdfjs", ["$timeout", ($timeout) ->
+		return {
+			scope: {
+					"pdfSrc": "="
+					"highlights": "="
+					"position": "="
+					"dblClickCallback": "="
+			}
+			link: (scope, element, attrs) ->
+				# pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
+				#		textLayerBuilder: TextLayerBuilder
+				#		annotationsLayerBuilder: AnnotationsLayerBuilder
+				#		highlightsLayerBuilder: HighlightsLayerBuilder
+				#		ondblclick: (e) -> onDoubleClick(e)
+				#		# logLevel: PDFListView.Logger.DEBUG
+				# pdfListView.listView.pageWidthOffset = 20
+				# pdfListView.listView.pageHeightOffset = 20
+
+				scope.loading = false
+				scope.scale = {}
+				initializedPosition = false
+				initializePosition = () ->
+					return if initializedPosition
+					initializedPosition = true
+
+					if (scale = $.localStorage("pdf.scale"))?
+						#pdfListView.setScaleMode(scale.scaleMode, scale.scale)
+					else
+						scope.scale = { scaleMode: 'scale_mode_fit_width' }
+
+					if (position = $.localStorage("pdf.position.#{attrs.key}"))
+						1
+						#pdfListView.setPdfPosition(position)
+
+					#scope.position = pdfListView.getPdfPosition(true)
+
+					$(window).unload () =>
+						$.localStorage "pdf.scale", {
+#							scaleMode: pdfListView.getScaleMode()
+#							scale: pdfListView.getScale()
+						}
+#						$.localStorage "pdf.position.#{attrs.key}", pdfListView.getPdfPosition()
+
+				flashControls = () ->
+					scope.flashControls = true
+					$timeout () ->
+						scope.flashControls = false
+					, 1000
+
+				element.find(".pdfjs-viewer").scroll () ->
+#					scope.position = pdfListView.getPdfPosition(true)
+
+				onDoubleClick = (e) ->
+					scope.dblClickCallback?(page: e.page, offset: { top: e.y, left: e.x })
+
+				scope.$watch "pdfSrc", (url) ->
+					if url
+						scope.loading = true
+						scope.progress = 0
+						console.log 'pdfSrc =', url
+						initializePosition()
+						flashControls()
+						scope.$broadcast 'layout-ready'
+						# pdfListView
+						#		.loadPdf(url, onProgress)
+						#		.then () ->
+						#			scope.$apply () ->
+						#				scope.loading = false
+						#				delete scope.progress
+						#				initializePosition()
+						#				flashControls()
+
+				scope.$watch "highlights", (areas) ->
+					return if !areas?
+					highlights = for area in areas or []
+						{
+							page: area.page - 1
+							highlight:
+								left: area.h
+								top: area.v
+								height: area.height
+								width: area.width
+						}
+
+					if highlights.length > 0
+						first = highlights[0]
+					#		pdfListView.setPdfPosition({
+					#			page: first.page
+					#			offset:
+					#				left: first.highlight.left
+					#				top: first.highlight.top - 80
+					#		}, true)
+
+					# pdfListView.clearHighlights()
+					# pdfListView.setHighlights(highlights, true)
+
+					# setTimeout () =>
+					#		pdfListView.clearHighlights()
+					# , 1000
+
+				scope.fitToHeight = () ->
+#					pdfListView.setToFitHeight()
+
+				scope.fitToWidth = () ->
+#					pdfListView.setToFitWidth()
+
+				scope.zoomIn = () ->
+#					scale = pdfListView.getScale()
+#					pdfListView.setScale(scale * 1.2)
+
+				scope.zoomOut = () ->
+#					scale = pdfListView.getScale()
+#					pdfListView.setScale(scale / 1.2)
+
+				if attrs.resizeOn?
+					for event in attrs.resizeOn.split(",")
+						scope.$on event, (e) ->
+							console.log 'got a resize event', event, e
+#							pdfListView.onResize()
+
+			template: """
+				<div data-pdf-viewer class="pdfjs-viewer" pdf-src='pdfSrc' position='position' scale='scale'></div>
+				<div class="pdfjs-controls" ng-class="{'flash': flashControls }">
+					<div class="btn-group">
+						<a href
+							class="btn btn-info btn-lg"
+							ng-click="fitToWidth()"
+							tooltip="Fit to Width"
+							tooltip-append-to-body="true"
+							tooltip-placement="bottom"
+						>
+							<i class="fa fa-fw fa-arrows-h"></i>
+						</a>
+						<a href
+							class="btn btn-info btn-lg"
+							ng-click="fitToHeight()"
+							tooltip="Fit to Height"
+							tooltip-append-to-body="true"
+							tooltip-placement="bottom"
+						>
+							<i class="fa fa-fw fa-arrows-v"></i>
+						</a>
+						<a href
+							class="btn btn-info btn-lg"
+							ng-click="zoomIn()"
+							tooltip="Zoom In"
+							tooltip-append-to-body="true"
+							tooltip-placement="bottom"
+						>
+							<i class="fa fa-fw fa-search-plus"></i>
+						</a>
+						<a href
+							class="btn btn-info btn-lg"
+							ng-click="zoomOut()"
+							tooltip="Zoom Out"
+							tooltip-append-to-body="true"
+							tooltip-placement="bottom"
+						>
+							<i class="fa fa-fw fa-search-minus"></i>
+						</a>
+					</div>
+				</div>
+			"""
+		}
+	]

+ 80 - 0
services/web/public/coffee/ide/pdfng/directives/pdfPage.coffee

@@ -0,0 +1,80 @@
+app = angular.module 'pdfPage', []
+
+app.directive 'pdfPage', ['$timeout', ($timeout) ->
+	{
+		require: '^pdfViewer',
+		template: '''
+		<div class="pdf-canvas"></div>
+		<div class="plv-text-layer text-layer"></div>
+		<div class="plv-annotations-layer annotations-layer"></div>
+		<div class="plv-highlights-layer highlights-layer"></div>
+		'''
+		link: (scope, element, attrs, ctrl) ->
+			canvasElement = $(element).find('.pdf-canvas')
+			textElement = $(element).find('.text-layer')
+			annotationsElement = $(element).find('.annotations-layer')
+			highlightsElement = $(element).find('.highlights-layer')
+
+			updatePageSize = (size) ->
+				element.height(Math.floor(size[0]))
+				element.width(Math.floor(size[1]))
+				scope.page.sized = true
+
+			isVisible = (containerSize) ->
+				elemTop = element.offset().top - containerSize[2]
+				elemBottom = elemTop + element.innerHeight()
+				visible = (elemTop < containerSize[1] and elemBottom > 0)
+				scope.page.visible = visible
+				scope.page.elemTop = elemTop
+				scope.page.elemBottom = elemBottom
+				return visible
+
+			renderPage = () ->
+				scope.document.renderPage {
+					canvas: canvasElement,
+					text: textElement
+					annotations: annotationsElement
+					highlights: highlightsElement
+				}, scope.page.pageNum
+
+			pausePage = () ->
+				scope.document.pause {
+					canvas: canvasElement,
+					text: textElement
+				}, scope.page.pageNum
+
+			# keep track of our page element, so we can access it in the
+			# parent with scope.pages[i].element
+			scope.page.element = element
+
+			if (!scope.page.sized && scope.defaultPageSize)
+				updatePageSize scope.defaultPageSize
+
+			if scope.page.current
+					console.log 'we must scroll to this page', scope.page.pageNum,
+						'at position', scope.page.position
+					renderPage()
+					# this is the current page, we want to scroll it into view
+					scope.document.getPdfViewport(scope.page.pageNum).then (viewport) ->
+						scope.page.viewport = viewport
+						ctrl.setPdfPosition(scope.page, scope.page.position)
+
+			scope.$watch 'defaultPageSize', (defaultPageSize) ->
+				return unless defaultPageSize?
+				updatePageSize defaultPageSize
+
+			watchHandle = scope.$watch 'containerSize', (containerSize, oldVal) ->
+				return unless containerSize?
+				return unless scope.page.sized
+				oldVisible = scope.page.visible
+				newVisible = isVisible containerSize
+				scope.page.visible = newVisible
+				if newVisible && !oldVisible
+					renderPage()
+					# TODO deregister this listener after the page is rendered
+					#watchHandle()
+				else if !newVisible && oldVisible
+					pausePage()
+
+	}
+]

+ 185 - 0
services/web/public/coffee/ide/pdfng/directives/pdfRenderer.coffee

@@ -0,0 +1,185 @@
+app = angular.module 'PDFRenderer', ['pdfAnnotations', 'pdfTextLayer']
+
+console.log 'hello from Renderer'
+
+app.factory 'PDFRenderer', ['$q', '$timeout', 'pdfAnnotations', 'pdfTextLayer', ($q, $timeout, pdfAnnotations, pdfTextLayer) ->
+
+	class PDFRenderer
+		@JOB_QUEUE_INTERVAL: 100
+
+		constructor: (@url, @options) ->
+			@scale = @options.scale || 1
+			@document = $q.when(PDFJS.getDocument @url)
+			@navigateFn = @options.navigateFn
+			@resetState()
+
+		resetState: () ->
+			console.log 'reseting renderer state'
+			@page = []
+			@complete = []
+			@timeout = []
+			@renderTask = []
+			@renderQueue = []
+			@jobs = 0
+
+		getNumPages: () ->
+			@document.then (pdfDocument) ->
+				pdfDocument.numPages
+
+		getPage: (pageNum) ->
+			# with promise caching
+			return @page[pageNum] if @page[pageNum]?
+			@page[pageNum] = @document.then (pdfDocument) ->
+				pdfDocument.getPage(pageNum)
+
+		getPdfViewport: (pageNum, scale) ->
+			scale ?= @scale
+			@document.then (pdfDocument) ->
+				pdfDocument.getPage(pageNum).then (page) ->
+					viewport = page.getViewport scale
+
+		getDestinations: () ->
+			@document.then (pdfDocument) ->
+				pdfDocument.getDestinations()
+
+		getPageIndex: (ref) ->
+			@document.then (pdfDocument) ->
+				pdfDocument.getPageIndex(ref).then (idx) ->
+					idx
+
+		getScale: () ->
+			@scale
+
+		setScale: (@scale) ->
+			@resetState()
+
+		pause: (element, pagenum) ->
+			return if @complete[pagenum]
+			@renderQueue = @renderQueue.filter (q) ->
+				q.pagenum != pagenum
+			@stopSpinner (element.canvas)
+
+		triggerRenderQueue: () ->
+			$timeout () =>
+				@processRenderQueue()
+			, @JOB_QUEUE_INTERVAL
+
+		removeCompletedJob: (pagenum) ->
+			# may need to clean up deferred object here
+			delete @renderTask[pagenum]
+			@jobs = @jobs - 1
+			@triggerRenderQueue()
+
+		renderPage: (element, pagenum) ->
+			viewport = $q.defer()
+			current = {
+				'element': element
+				'pagenum': pagenum
+			}
+			@renderQueue.push(current)
+			@triggerRenderQueue()
+
+		processRenderQueue: () ->
+			return if @jobs > 0
+			current = @renderQueue.pop()
+			return unless current?
+			[element, pagenum] = [current.element, current.pagenum]
+			return if @complete[pagenum]
+			return if @renderTask[pagenum]
+			@jobs = @jobs + 1
+
+			@addSpinner(element.canvas)
+
+			pageLoad = @getPage(pagenum)
+
+			@renderTask[pagenum] = pageLoad.then (pageObject) =>
+				@doRender element, pagenum, pageObject
+
+			@renderTask[pagenum].then () =>
+				# complete
+				@complete[pagenum] = true
+				@removeCompletedJob pagenum
+			, () =>
+				# rejected
+				@removeCompletedJob pagenum
+
+		doRender: (element, pagenum, page) ->
+			self = this
+			scale = @scale
+
+			if (not scale?)
+				console.log 'scale is undefined, returning'
+				return
+
+			canvas = $('<canvas class="pdf-canvas-new"></canvas>')
+
+			viewport = page.getViewport (scale)
+
+			devicePixelRatio = window.devicePixelRatio || 1
+
+			ctx = canvas[0].getContext '2d'
+			backingStoreRatio = ctx.webkitBackingStorePixelRatio ||
+				ctx.mozBackingStorePixelRatio ||
+				ctx.msBackingStorePixelRatio ||
+				ctx.oBackingStorePixelRatio ||
+				ctx.backingStorePixelRatio || 1
+			pixelRatio = devicePixelRatio / backingStoreRatio
+
+			scaledWidth = (Math.floor(viewport.width) * pixelRatio) | 0
+			scaledHeight = (Math.floor(viewport.height) * pixelRatio) | 0
+
+			newWidth = Math.floor(viewport.width)
+			newHeight = Math.floor(viewport.height)
+
+			canvas[0].height = scaledHeight
+			canvas[0].width = scaledWidth
+
+			canvas.height(newHeight + 'px')
+			canvas.width(newWidth + 'px')
+
+			element.canvas[0].height = newHeight
+			element.canvas[0].width = newWidth
+
+			if pixelRatio != 1
+				ctx.scale(pixelRatio, pixelRatio)
+
+			textLayer = new pdfTextLayer({
+				textLayerDiv: element.text[0]
+				viewport: viewport
+			})
+			page.getTextContent().then (textContent) ->
+				console.log 'text content is', textContent
+				window.RENDER_DELAY = 0
+				textLayer.setTextContent textContent
+
+			annotationsLayer = new pdfAnnotations({
+				annotations: element.annotations[0]
+				viewport: viewport
+				navigateFn: @navigateFn
+			})
+			page.getAnnotations().then (annotations) ->
+				console.log 'annotations are', annotations
+				window.RENDER_DELAY = 0
+				annotationsLayer.setAnnotations annotations
+
+			return @renderTask = page.render {
+				canvasContext: ctx
+				viewport: viewport
+			}
+			.then () ->
+				element.canvas.replaceWith(canvas)
+				canvas.removeClass('pdf-canvas-new')
+
+		addSpinner: (element) ->
+			element.css({position: 'relative'})
+			h = element.parent().height()
+			w = element.parent().width()
+			size = Math.floor(0.5 * Math.min(h, w))
+			spinner = $('<div style="position: absolute; top: 50%; left:50%; transform: translateX(-50%) translateY(50%);"><i class="fa fa-spinner fa-spin" style="color: #999"></i></div>')
+			spinner.css({'font-size' : size + 'px'})
+			element.append(spinner)
+
+		stopSpinner: (element) ->
+			element.find('.fa-spin').removeClass('fa-spin')
+
+	]

+ 207 - 0
services/web/public/coffee/ide/pdfng/directives/pdfTextLayer.coffee

@@ -0,0 +1,207 @@
+app = angular.module 'pdfTextLayer', []
+
+app.factory 'pdfTextLayer', [ () ->
+
+# TRANSLATED FROM pdf.js-1.0.712
+# pdf.js-1.0.712/web/ui_utils.js
+# pdf.js-1.0.712/web/text_layer_builder.js
+
+# -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+
+# Copyright 2012 Mozilla Foundation
+# *
+# * Licensed under the Apache License, Version 2.0 (the "License");
+# * you may not use this file except in compliance with the License.
+# * You may obtain a copy of the License at
+# *
+# *     http://www.apache.org/licenses/LICENSE-2.0
+# *
+# * Unless required by applicable law or agreed to in writing, software
+# * distributed under the License is distributed on an "AS IS" BASIS,
+# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# * See the License for the specific language governing permissions and
+# * limitations under the License.
+#
+
+# globals CustomStyle, scrollIntoView, PDFJS
+# ms
+
+# optimised CSS custom property getter/setter
+
+	CustomStyle = (CustomStyleClosure = ->
+
+		# As noted on: http://www.zachstronaut.com/posts/2009/02/17/
+		#              animate-css-transforms-firefox-webkit.html
+		# in some versions of IE9 it is critical that ms appear in this list
+		# before Moz
+		CustomStyle = ->
+		prefixes = [
+			'ms'
+			'Moz'
+			'Webkit'
+			'O'
+		]
+		_cache = {}
+		CustomStyle.getProp = get = (propName, element) ->
+
+			# check cache only when no element is given
+			return _cache[propName]	if arguments.length is 1 and typeof _cache[propName] is 'string'
+			element = element or document.documentElement
+			style = element.style
+			prefixed = undefined
+			uPropName = undefined
+
+			# test standard property first
+			return (_cache[propName] = propName)	if typeof style[propName] is 'string'
+
+			# capitalize
+			uPropName = propName.charAt(0).toUpperCase() + propName.slice(1)
+
+			# test vendor specific properties
+			i = 0
+			l = prefixes.length
+
+			while i < l
+				prefixed = prefixes[i] + uPropName
+				return (_cache[propName] = prefixed)	if typeof style[prefixed] is 'string'
+				i++
+
+			#if all fails then set to undefined
+			_cache[propName] = 'undefined'
+
+		CustomStyle.setProp = set = (propName, element, str) ->
+			prop = @getProp(propName)
+			element.style[prop] = str	if prop isnt 'undefined'
+			return
+
+		CustomStyle
+	)()
+
+	#################################
+
+	isAllWhitespace = (str) ->
+		not NonWhitespaceRegexp.test(str)
+	'use strict'
+	FIND_SCROLL_OFFSET_TOP = -50
+	FIND_SCROLL_OFFSET_LEFT = -400
+	MAX_TEXT_DIVS_TO_RENDER = 100000
+	RENDER_DELAY = 200
+	NonWhitespaceRegexp = /\S/
+
+	###*
+	TextLayerBuilder provides text-selection functionality for the PDF.
+	It does this by creating overlay divs over the PDF text. These divs
+	contain text that matches the PDF text they are overlaying. This object
+	also provides a way to highlight text that is being searched for.
+	###
+
+	class pdfTextLayer
+
+		constructor: (options) ->
+			@textLayerDiv = options.textLayerDiv
+			@layoutDone = false
+			@divContentDone = false
+			@pageIdx = options.pageIndex
+			@matches = []
+			@lastScrollSource = options.lastScrollSource or null
+			@viewport = options.viewport
+			@isViewerInPresentationMode = options.isViewerInPresentationMode
+			@textDivs = []
+			@findController = options.findController or null
+
+		renderLayer: () ->
+			textLayerFrag = document.createDocumentFragment()
+			textDivs = @textDivs
+			textDivsLength = textDivs.length
+			canvas = document.createElement('canvas')
+			ctx = canvas.getContext('2d')
+
+			# No point in rendering many divs as it would make the browser
+			# unusable even after the divs are rendered.
+			return	if textDivsLength > MAX_TEXT_DIVS_TO_RENDER
+			lastFontSize = undefined
+			lastFontFamily = undefined
+			i = 0
+
+			while i < textDivsLength
+				textDiv = textDivs[i]
+				continue if textDiv.dataset.isWhitespace
+				fontSize = textDiv.style.fontSize
+				fontFamily = textDiv.style.fontFamily
+
+				# Only build font string and set to context if different from last.
+				if fontSize isnt lastFontSize or fontFamily isnt lastFontFamily
+					ctx.font = fontSize + ' ' + fontFamily
+					lastFontSize = fontSize
+					lastFontFamily = fontFamily
+				width = ctx.measureText(textDiv.textContent).width
+				if width > 0
+					textLayerFrag.appendChild textDiv
+
+					# Dataset values come of type string.
+					textScale = textDiv.dataset.canvasWidth / width
+					rotation = textDiv.dataset.angle
+					transform = 'scale(' + textScale + ', 1)'
+					transform = 'rotate(' + rotation + 'deg) ' + transform	if rotation
+					CustomStyle.setProp 'transform', textDiv, transform
+					CustomStyle.setProp 'transformOrigin', textDiv, '0% 0%'
+				i++
+			@textLayerDiv.appendChild textLayerFrag
+			return
+
+		appendText: (geom, styles) ->
+			style = styles[geom.fontName]
+			textDiv = document.createElement('div')
+			@textDivs.push textDiv
+			if isAllWhitespace(geom.str)
+				textDiv.dataset.isWhitespace = true
+				return
+			tx = PDFJS.Util.transform(@viewport.transform, geom.transform)
+			angle = Math.atan2(tx[1], tx[0])
+			angle += Math.PI / 2	if style.vertical
+			fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]))
+			fontAscent = fontHeight
+			if style.ascent
+				fontAscent = style.ascent * fontAscent
+			else fontAscent = (1 + style.descent) * fontAscent	if style.descent
+			left = undefined
+			top = undefined
+			if angle is 0
+				left = tx[4]
+				top = tx[5] - fontAscent
+			else
+				left = tx[4] + (fontAscent * Math.sin(angle))
+				top = tx[5] - (fontAscent * Math.cos(angle))
+			textDiv.style.left = left + 'px'
+			textDiv.style.top = top + 'px'
+			textDiv.style.fontSize = fontHeight + 'px'
+			textDiv.style.fontFamily = style.fontFamily
+			textDiv.textContent = geom.str
+
+			# |fontName| is only used by the Font Inspector. This test will succeed
+			# when e.g. the Font Inspector is off but the Stepper is on, but it's
+			# not worth the effort to do a more accurate test.
+			textDiv.dataset.fontName = geom.fontName	if PDFJS.pdfBug
+
+			# Storing into dataset will convert number into string.
+			textDiv.dataset.angle = angle * (180 / Math.PI)	if angle isnt 0
+			if style.vertical
+				textDiv.dataset.canvasWidth = geom.height * @viewport.scale
+			else
+				textDiv.dataset.canvasWidth = geom.width * @viewport.scale
+			return
+
+		setTextContent: (textContent) ->
+			@textContent = textContent
+			textItems = textContent.items
+			i = 0
+			len = textItems.length
+
+			while i < len
+				@appendText textItems[i], textContent.styles
+				i++
+			@divContentDone = true
+			@renderLayer()
+			return
+
+]

+ 357 - 0
services/web/public/coffee/ide/pdfng/directives/pdfViewer.coffee

@@ -0,0 +1,357 @@
+app = angular.module 'pdfViewerApp', ['pdfPage', 'PDFRenderer', 'pdfHighlights']
+
+app.config [ "$logProvider", ($logProvider) ->
+	$logProvider.debugEnabled true
+]
+
+console.log "HELLO"
+
+app.controller 'pdfViewerController', ['$scope', '$q', 'PDFRenderer', '$element', 'pdfHighlights', ($scope, $q, PDFRenderer, $element, pdfHighlights) ->
+	@load = () ->
+		$scope.document = new PDFRenderer($scope.pdfSrc, {
+			scale: 1,
+			navigateFn: (ref) ->
+				# this function captures clicks on the annotation links
+				$scope.navigateTo = ref
+				$scope.$apply()
+		})
+
+		# we will have all the main information needed to start display
+		# after the following promise is resolved
+		$scope.loaded = $q.all({
+			numPages: $scope.document.getNumPages()
+			destinations: $scope.document.getDestinations()
+			# get size of first page as default @ scale 1
+			pdfViewport: $scope.document.getPdfViewport 1, 1
+			}).then (result) ->
+				$scope.pdfViewport = result.pdfViewport
+				$scope.pdfPageSize = [
+					result.pdfViewport.height,
+					result.pdfViewport.width
+				]
+				$scope.destinations = result.destinations
+				console.log 'resolved q.all, page size is', result
+				$scope.numPages = result.numPages
+
+	@setScale = (scale, containerHeight, containerWidth) ->
+		$scope.loaded.then () ->
+			scale = {} if not scale?
+			if scale.scaleMode == 'scale_mode_fit_width'
+				# TODO make this dynamic
+				numScale = (containerWidth - 15) / ($scope.pdfPageSize[1])
+			else if scale.scaleMode == 'scale_mode_fit_height'
+				# TODO magic numbers for jquery ui layout
+				numScale = (containerHeight) / ($scope.pdfPageSize[0])
+			else if scale.scaleMode == 'scale_mode_value'
+				numScale = scale.scale
+			else if scale.scaleMode == 'scale_mode_auto'
+				# TODO
+			else
+				scale.scaleMode = 'scale_mode_fit_width'
+				numScale = (containerWidth - 15) / ($scope.pdfPageSize[1])
+				# TODO
+			$scope.scale.scale = numScale
+			$scope.document.setScale(numScale)
+			$scope.defaultPageSize = [
+				numScale * $scope.pdfPageSize[0],
+				numScale * $scope.pdfPageSize[1]
+			]
+			console.log 'in setScale result', $scope.scale.scale, $scope.defaultPageSize
+
+	@redraw = (position) ->
+		console.log 'in redraw'
+		console.log 'reseting pages array for', $scope.numPages
+		console.log 'position is', position
+		$scope.pages = ({
+			pageNum: i
+		} for i in [1 .. $scope.numPages])
+		if position? && position.page?
+			console.log 'setting current page', position.page
+			pagenum = position.page
+			$scope.pages[pagenum - 1].current = true
+			$scope.pages[pagenum - 1].position = position
+
+	@zoomIn = () ->
+		console.log 'zoom in'
+		newScale = $scope.scale.scale * 1.2
+		$scope.forceScale = { scaleMode: 'scale_mode_value', scale: newScale }
+
+	@zoomOut = () ->
+		console.log 'zoom out'
+		newScale = $scope.scale.scale / 1.2
+		$scope.forceScale = { scaleMode: 'scale_mode_value', scale: newScale }
+
+	@fitWidth = () ->
+		console.log 'fit width'
+		$scope.forceScale = { scaleMode: 'scale_mode_fit_width' }
+
+	@fitHeight = () ->
+		console.log 'fit height'
+		$scope.forceScale = { scaleMode: 'scale_mode_fit_height' }
+
+	@checkPosition = () ->
+		console.log 'check position'
+		$scope.forceCheck = ($scope.forceCheck || 0) + 1
+
+	@showRandomHighlights = () ->
+		console.log 'show highlights'
+		$scope.highlights = [
+			{
+				page: 3
+				h: 100
+				v: 100
+				height: 30
+				width: 200
+			}
+		]
+
+	# we work with (pagenumber, % of height down page from top)
+	# pdfListView works with (pagenumber, vertical position up page from
+	# bottom measured in pts)
+
+	@getPdfPosition = () ->
+		console.log 'in getPdfPosition'
+		visiblePages = $scope.pages.filter (page) ->
+			page.visible
+		if visiblePages.length
+			topPage = visiblePages[0]
+		else
+			console.log 'CANNOT FIND TOP PAGE'
+			topPage = $scope.pages[0]
+		console.log 'top page is', topPage.pageNum, topPage.elemTop, topPage.elemBottom, topPage
+		top = topPage.elemTop
+		bottom = topPage.elemBottom
+		viewportTop = 0
+		viewportHeight = $element.height()
+		topVisible = (top >= viewportTop && top < viewportTop + viewportHeight)
+		someContentVisible = (top < viewportTop && bottom > viewportTop)
+		console.log 'in PdfListView', top, topVisible, someContentVisible, viewportTop
+		if topVisible
+			canvasOffset = 0
+		else if someContentVisible
+			canvasOffset = viewportTop - top
+		else
+			canvasOffset = null
+		console.log 'pdfListview position = ', canvasOffset
+		# instead of using promise, check if size is known and revert to
+		# default otherwise
+		console.log 'looking up viewport', topPage.viewport, $scope.pdfViewport
+		if topPage.viewport
+			viewport = topPage.viewport
+			pdfOffset = viewport.convertToPdfPoint(0, canvasOffset);
+		else
+			console.log 'WARNING: had to default to global page size'
+			viewport = $scope.pdfViewport
+			scaledOffset = canvasOffset / $scope.scale.scale
+			pdfOffset = viewport.convertToPdfPoint(0, scaledOffset);
+		console.log 'converted to offset = ', pdfOffset
+		newPosition = {
+			"page": topPage.pageNum,
+			"offset" : { "top" : pdfOffset[1], "left": 0	}
+		}
+		return newPosition
+
+	@computeOffset = (page, position) ->
+		element = page.element
+		pageTop = $(element).offset().top - $(element).parent().offset().top
+		console.log('top of page scroll is', pageTop)
+		console.log('inner height is', $(element).innerHeight())
+		offset = position.offset
+		# convert offset to pixels
+		return $scope.document.getPdfViewport(page.pageNum).then (viewport) ->
+			page.viewport = viewport
+			pageOffset = viewport.convertToViewportPoint(offset.left, offset.top)
+			console.log 'addition offset =', pageOffset
+			console.log 'total', pageTop + pageOffset[1]
+			Math.round(pageTop + pageOffset[1] + 10) ## 10 is margin
+
+	@setPdfPosition = (page, position) ->
+		console.log 'required pdf Position is', position
+		@computeOffset(page, position).then (offset) ->
+			$scope.pleaseScrollTo =  offset
+			$scope.position = position
+
+	return this
+
+]
+
+app.directive 'pdfViewer', ['$q', '$timeout', ($q, $timeout) ->
+	{
+		controller: 'pdfViewerController'
+		controllerAs: 'ctrl'
+		scope: {
+			"pdfSrc": "="
+			"highlights": "="
+			"position": "="
+			"scale": "="
+			"dblClickCallback": "="
+		}
+		template: """
+		<div data-pdf-page class='pdf-page-container plv-page-view page-view' ng-repeat='page in pages'></div>
+		"""
+		link: (scope, element, attrs, ctrl) ->
+			console.log 'in pdfViewer element is', element
+			console.log 'attrs', attrs
+			layoutReady = $q.defer()
+			layoutReady.notify 'waiting for layout'
+			layoutReady.promise.then () ->
+				console.log 'layoutReady was resolved'
+
+			# TODO can we combine this with scope.parentSize, need to finalize boxes
+			updateContainer = () ->
+				scope.containerSize = [
+					element.innerWidth()
+					element.innerHeight()
+					element.offset().top
+			]
+
+			doRescale = (scale) ->
+				console.log 'doRescale', scale
+				origposition = angular.copy scope.position
+				console.log 'origposition', origposition
+				layoutReady.promise.then () ->
+					[h, w] = [element.innerHeight(), element.width()]
+					console.log 'in promise', h, w
+					ctrl.setScale(scale, h, w).then () ->
+						ctrl.redraw(origposition)
+
+			scope.$on 'layout-ready', () ->
+				console.log 'GOT LAYOUT READY EVENT'
+				console.log 'calling refresh'
+				updateContainer()
+				layoutReady.resolve 'layout is ready'
+				scope.parentSize = [
+					element.innerHeight(),
+					element.innerWidth()
+				]
+				#scope.$apply()
+
+			scope.$on 'layout:pdf:resize', () ->
+				console.log 'GOT LAYOUT-RESIZE EVENT'
+				scope.parentSize = [
+					element.innerHeight(),
+					element.innerWidth()
+				]
+				#scope.$apply()
+
+			element.on 'scroll', () ->
+				console.log 'scroll detected', scope.adjustingScroll
+				updateContainer()
+				scope.$apply()
+				#console.log 'pdfposition', element.parent().scrollTop()
+				if scope.adjustingScroll
+					scope.adjustingScroll = false
+					return
+				#console.log 'not from auto scroll'
+				scope.position = ctrl.getPdfPosition()
+				console.log 'position is', scope.position
+				scope.$apply()
+
+			scope.$watch 'pdfSrc', (newVal, oldVal) ->
+				console.log 'loading pdf', newVal, oldVal
+				return unless newVal?
+				ctrl.load()
+				doRescale scope.scale
+
+			scope.$watch 'scale', (newVal, oldVal) ->
+				# no need to set scale when initialising, done in pdfSrc
+				return if newVal == oldVal
+				console.log 'XXX calling Setscale in scale watch'
+				doRescale newVal
+
+			scope.$watch 'forceScale', (newVal, oldVal) ->
+				console.log 'got change in numscale watcher', newVal, oldVal
+				return unless newVal?
+				doRescale newVal
+
+			scope.$watch 'position', (newVal, oldVal) ->
+				console.log 'got change in position watcher', newVal, oldVal
+
+			scope.$watch 'forceCheck', (newVal, oldVal) ->
+				console.log 'forceCheck', newVal, oldVal
+				return unless newVal?
+				scope.adjustingScroll = true  # temporarily disable scroll
+				doRescale scope.scale
+
+			scope.$watch('parentSize', (newVal, oldVal) ->
+				console.log 'XXX in parentSize watch', newVal, oldVal
+				if newVal == oldVal
+					console.log 'returning because old and new are the same'
+					return
+				return unless oldVal?
+				console.log 'XXX calling setScale in parentSize watcher'
+				doRescale scope.scale
+			, true)
+
+			scope.$watch 'elementWidth', (newVal, oldVal) ->
+				console.log '*** watch INTERVAL element width is', newVal, oldVal
+
+			scope.$watch 'pleaseScrollTo', (newVal, oldVal) ->
+				console.log 'got request to ScrollTo', newVal, 'oldVal', oldVal
+				return unless newVal?
+				scope.adjustingScroll = true  # temporarily disable scroll
+																			# handler while we reposition
+				$(element).scrollTop(newVal)
+				scope.pleaseScrollTo = undefined
+
+			scope.$watch 'navigateTo', (newVal, oldVal) ->
+				return unless newVal?
+				console.log 'got request to navigate to', newVal, 'oldVal', oldVal
+				scope.navigateTo = undefined
+				console.log 'navigate to', newVal
+				console.log 'look up page num'
+				scope.loaded.then () ->
+					console.log 'destinations are', scope.destinations
+					r = scope.destinations[newVal.dest]
+					console.log 'need to go to', r
+					console.log 'page ref is', r[0]
+					scope.document.getPageIndex(r[0]).then (pidx) ->
+						console.log 'page num is', pidx
+						scope.document.getPdfViewport(pidx).then (viewport) ->
+							console.log 'got viewport', viewport
+							coords = viewport.convertToViewportPoint r[2], r[3]
+							console.log	'viewport position', coords
+							console.log 'r is', r, 'r[1]', r[1], 'r[1].name', r[1].name
+							if r[1].name == 'XYZ'
+								console.log 'XYZ:', r[2], r[3]
+								newPosition = {page: pidx + 1, offset: {top: r[3], left: r[2]}}
+								ctrl.setPdfPosition scope.pages[pidx], newPosition
+
+			scope.$watch "highlights", (areas) ->
+					return if !areas?
+					console.log 'areas are', areas
+					highlights = for area in areas or []
+						{
+							page: area.page - 1
+							highlight:
+								left: area.h
+								top: area.v
+								height: area.height
+								width: area.width
+						}
+					console.log 'highlights', highlights
+
+					if highlights.length > 0
+						first = highlights[0]
+						ctrl.setPdfPosition({
+							page: first.page
+							offset:
+								left: first.highlight.left
+								top: first.highlight.top - 80
+						}, true)
+
+					# iterate over pages
+					# highlightsElement = $(element).find('.highlights-layer')
+					# highlightsLayer = new pdfHighlights({
+					#		highlights: element.highlights[0]
+					#		viewport: viewport
+					# })
+					#pdfListView.clearHighlights()
+					#ctrl.setHighlights(highlights, true)
+
+					#setTimeout () =>
+					#	pdfListView.clearHighlights()
+					#, 1000
+
+	}
+]