/src/PanoJS.js

http://panojs.googlecode.com/ · JavaScript · 924 lines · 627 code · 122 blank · 175 comment · 153 complexity · 4911dd31d1d9f8c352c877899154ea34 MD5 · raw file

  1. /**
  2. * Panoramic JavaScript Image Viewer (PanoJS) 1.0.3-SVN
  3. *
  4. * Generates a draggable and zoomable viewer for images that would
  5. * be otherwise too large for a browser window. Examples would include
  6. * maps or high resolution document scans.
  7. *
  8. * Images must be precut into tiles, such as by the accompanying tilemaker.py
  9. * python library.
  10. *
  11. * <div class="viewer">
  12. * <div class="well"><!-- --></div>
  13. * <div class="surface"><!-- --></div>
  14. * <div class="controls">
  15. * <a href="#" class="zoomIn">+</a>
  16. * <a href="#" class="zoomOut">-</a>
  17. * </div>
  18. * </div>
  19. *
  20. * The "well" node is where generated IMG elements are appended. It
  21. * should have the CSS rule "overflow: hidden", to occlude image tiles
  22. * that have scrolled out of view.
  23. *
  24. * The "surface" node is the transparent mouse-responsive layer of the
  25. * image viewer, and should match the well in size.
  26. *
  27. * var viewerBean = new PanoJS(element, 'tiles', 256, 3, 1);
  28. *
  29. * To disable the image toolbar in IE, be sure to add the following:
  30. * <meta http-equiv="imagetoolbar" content="no" />
  31. *
  32. * Copyright (c) 2005 Michal Migurski <mike-gsv@teczno.com>
  33. * Dan Allen <dan.allen@mojavelinux.com>
  34. *
  35. * Redistribution and use in source form, with or without modification,
  36. * are permitted provided that the following conditions are met:
  37. * 1. Redistributions of source code must retain the above copyright
  38. * notice, this list of conditions and the following disclaimer.
  39. * 2. The name of the author may not be used to endorse or promote products
  40. * derived from this software without specific prior written permission.
  41. *
  42. * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
  43. * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  44. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  45. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
  46. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  47. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  48. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  49. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  50. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  51. * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  52. *
  53. * @author Michal Migurski <mike-gsv@teczno.com>
  54. * @author Dan Allen <dan.allen@mojavelinux.com>
  55. *
  56. * NOTE: if artifacts are appearing, then positions include half-pixels
  57. * TODO: additional jsdoc and package jsmin
  58. * TODO: Tile could be an object
  59. */
  60. function PanoJS(viewer, options) {
  61. // listeners that are notified on a move (pan) event
  62. this.viewerMovedListeners = [];
  63. // listeners that are notified on a zoom event
  64. this.viewerZoomedListeners = [];
  65. if (typeof viewer == 'string') {
  66. this.viewer = document.getElementById(viewer);
  67. }
  68. else {
  69. this.viewer = viewer;
  70. }
  71. if (typeof options == 'undefined') {
  72. options = {};
  73. }
  74. if (typeof options.tileUrlProvider != 'undefined' &&
  75. PanoJS.isInstance(options.tileUrlProvider, PanoJS.TileUrlProvider)) {
  76. this.tileUrlProvider = options.tileUrlProvider;
  77. }
  78. else {
  79. this.tileUrlProvider = new PanoJS.TileUrlProvider(
  80. options.tileBaseUri ? options.tileBaseUri : PanoJS.TILE_BASE_URI,
  81. options.tilePrefix ? options.tilePrefix : PanoJS.TILE_PREFIX,
  82. options.tileExtension ? options.tileExtension : PanoJS.TILE_EXTENSION
  83. );
  84. }
  85. this.tileSize = (options.tileSize ? options.tileSize : PanoJS.TILE_SIZE);
  86. // assign and do some validation on the zoom levels to ensure sanity
  87. this.zoomLevel = (typeof options.initialZoom == 'undefined' ? -1 : parseInt(options.initialZoom));
  88. this.maxZoomLevel = (typeof options.maxZoom == 'undefined' ? 0 : Math.abs(parseInt(options.maxZoom)));
  89. if (this.zoomLevel > this.maxZoomLevel) {
  90. this.zoomLevel = this.maxZoomLevel;
  91. }
  92. this.initialPan = (options.initialPan ? options.initialPan : PanoJS.INITIAL_PAN);
  93. this.initialized = false;
  94. this.surface = null;
  95. this.well = null;
  96. this.width = 0;
  97. this.height = 0;
  98. this.top = 0;
  99. this.left = 0;
  100. this.x = 0;
  101. this.y = 0;
  102. this.border = -1;
  103. this.mark = { 'x' : 0, 'y' : 0 };
  104. this.pressed = false;
  105. this.tiles = [];
  106. this.cache = {};
  107. var blankTile = options.blankTile ? options.blankTile : PanoJS.BLANK_TILE_IMAGE;
  108. var loadingTile = options.loadingTile ? options.loadingTile : PanoJS.LOADING_TILE_IMAGE;
  109. this.cache['blank'] = new Image();
  110. this.cache['blank'].src = blankTile;
  111. if (blankTile != loadingTile) {
  112. this.cache['loading'] = new Image();
  113. this.cache['loading'].src = loadingTile;
  114. }
  115. else {
  116. this.cache['loading'] = this.cache['blank'];
  117. }
  118. // employed to throttle the number of redraws that
  119. // happen while the mouse is moving
  120. this.moveCount = 0;
  121. this.slideMonitor = 0;
  122. this.slideAcceleration = 0;
  123. // add to viewer registry
  124. PanoJS.VIEWERS[PanoJS.VIEWERS.length] = this;
  125. }
  126. // project specific variables
  127. PanoJS.PROJECT_NAME = 'PanoJS';
  128. PanoJS.PROJECT_VERSION = '1.0.0';
  129. PanoJS.REVISION_FLAG = '';
  130. // CSS definition settings
  131. PanoJS.SURFACE_STYLE_CLASS = 'surface';
  132. PanoJS.WELL_STYLE_CLASS = 'well';
  133. PanoJS.CONTROLS_STYLE_CLASS = 'controls'
  134. PanoJS.TILE_STYLE_CLASS = 'tile';
  135. // language settings
  136. PanoJS.MSG_BEYOND_MIN_ZOOM = 'Cannot zoom out past the current level.';
  137. PanoJS.MSG_BEYOND_MAX_ZOOM = 'Cannot zoom in beyond the current level.';
  138. // defaults if not provided as constructor options
  139. PanoJS.TILE_BASE_URI = 'tiles';
  140. PanoJS.TILE_PREFIX = 'tile-';
  141. PanoJS.TILE_EXTENSION = 'jpg';
  142. PanoJS.TILE_SIZE = 256;
  143. PanoJS.BLANK_TILE_IMAGE = 'blank.gif';
  144. PanoJS.LOADING_TILE_IMAGE = 'blank.gif';
  145. PanoJS.INITIAL_PAN = { 'x' : .5, 'y' : .5 };
  146. PanoJS.USE_LOADER_IMAGE = true;
  147. PanoJS.USE_SLIDE = true;
  148. PanoJS.USE_KEYBOARD = true;
  149. // performance tuning variables
  150. PanoJS.MOVE_THROTTLE = 3;
  151. PanoJS.SLIDE_DELAY = 40;
  152. PanoJS.SLIDE_ACCELERATION_FACTOR = 5;
  153. // the following are calculated settings
  154. PanoJS.DOM_ONLOAD = (navigator.userAgent.indexOf('KHTML') >= 0 ? false : true);
  155. PanoJS.GRAB_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'pointer' : (document.attachEvent ? 'url(grab.cur)' : '-moz-grab'));
  156. PanoJS.GRABBING_MOUSE_CURSOR = (navigator.userAgent.search(/KHTML|Opera/i) >= 0 ? 'move' : (document.attachEvent ? 'url(grabbing.cur)' : '-moz-grabbing'));
  157. // registry of all known viewers
  158. PanoJS.VIEWERS = [];
  159. // utility functions
  160. PanoJS.isInstance = function(object, clazz) {
  161. // FIXME: can this just be replaced with instanceof operator? It has been reported that __proto__ is specific to Netscape
  162. while (object != null) {
  163. if (object == clazz.prototype) {
  164. return true;
  165. }
  166. object = object.__proto__;
  167. }
  168. return false;
  169. }
  170. PanoJS.prototype = {
  171. /**
  172. * Resize the viewer to fit snug inside the browser window (or frame),
  173. * spacing it from the edges by the specified border.
  174. *
  175. * This method should be called prior to init()
  176. * FIXME: option to hide viewer to prevent scrollbar interference
  177. */
  178. fitToWindow : function(border) {
  179. if (typeof border != 'number' || border < 0) {
  180. border = 0;
  181. }
  182. this.border = border;
  183. var calcWidth = 0;
  184. var calcHeight = 0;
  185. if (window.innerWidth) {
  186. calcWidth = window.innerWidth;
  187. calcHeight = window.innerHeight;
  188. }
  189. else {
  190. calcWidth = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientWidth : document.body.clientWidth);
  191. calcHeight = (document.compatMode == 'CSS1Compat' ? document.documentElement.clientHeight : document.body.clientHeight);
  192. }
  193. calcWidth = Math.max(calcWidth - 2 * border, 0);
  194. calcHeight = Math.max(calcHeight - 2 * border, 0);
  195. if (calcWidth % 2) {
  196. calcWidth--;
  197. }
  198. if (calcHeight % 2) {
  199. calcHeight--;
  200. }
  201. this.width = calcWidth;
  202. this.height = calcHeight;
  203. this.viewer.style.width = this.width + 'px';
  204. this.viewer.style.height = this.height + 'px';
  205. this.viewer.style.top = border + 'px';
  206. this.viewer.style.left = border + 'px';
  207. },
  208. init : function() {
  209. if (document.attachEvent) {
  210. document.body.ondragstart = function() { return false; }
  211. }
  212. if (this.width == 0 && this.height == 0) {
  213. this.width = this.viewer.offsetWidth;
  214. this.height = this.viewer.offsetHeight;
  215. }
  216. var fullSize = this.tileSize;
  217. // explicit set of zoom level
  218. if (this.zoomLevel >= 0 && this.zoomLevel <= this.maxZoomLevel) {
  219. fullSize = this.tileSize * Math.pow(2, this.zoomLevel);
  220. }
  221. // calculate the zoom level based on what fits best in window
  222. else {
  223. this.zoomLevel = -1;
  224. fullSize = this.tileSize / 2;
  225. do {
  226. this.zoomLevel += 1;
  227. fullSize *= 2;
  228. } while (fullSize < Math.max(this.width, this.height));
  229. // take into account picture smaller than window size
  230. if (this.zoomLevel > this.maxZoomLevel) {
  231. var diff = this.zoomLevel - this.maxZoomLevel;
  232. this.zoomLevel = this.maxZoomLevel;
  233. fullSize /= Math.pow(2, diff);
  234. }
  235. }
  236. // move top level up and to the left so that the image is centered
  237. this.x = Math.floor((fullSize - this.width) * -this.initialPan.x);
  238. this.y = Math.floor((fullSize - this.height) * -this.initialPan.y);
  239. // offset of viewer in the window
  240. for (var node = this.viewer; node; node = node.offsetParent) {
  241. this.top += node.offsetTop;
  242. this.left += node.offsetLeft;
  243. }
  244. for (var child = this.viewer.firstChild; child; child = child.nextSibling) {
  245. if (child.className == PanoJS.SURFACE_STYLE_CLASS) {
  246. this.surface = child;
  247. child.backingBean = this;
  248. }
  249. else if (child.className == PanoJS.WELL_STYLE_CLASS) {
  250. this.well = child;
  251. child.backingBean = this;
  252. }
  253. else if (child.className == PanoJS.CONTROLS_STYLE_CLASS) {
  254. for (var control = child.firstChild; control; control = control.nextSibling) {
  255. if (control.className) {
  256. control.onclick = PanoJS[control.className + 'Handler'];
  257. }
  258. }
  259. }
  260. }
  261. this.viewer.backingBean = this;
  262. this.surface.style.cursor = PanoJS.GRAB_MOUSE_CURSOR;
  263. this.prepareTiles();
  264. this.initialized = true;
  265. },
  266. prepareTiles : function() {
  267. var rows = Math.ceil(this.height / this.tileSize) + 1;
  268. var cols = Math.ceil(this.width / this.tileSize) + 1;
  269. for (var c = 0; c < cols; c++) {
  270. var tileCol = [];
  271. for (var r = 0; r < rows; r++) {
  272. /**
  273. * element is the DOM element associated with this tile
  274. * posx/posy are the pixel offsets of the tile
  275. * xIndex/yIndex are the index numbers of the tile segment
  276. * qx/qy represents the quadrant location of the tile
  277. */
  278. var tile = {
  279. 'element' : null,
  280. 'posx' : 0,
  281. 'posy' : 0,
  282. 'xIndex' : c,
  283. 'yIndex' : r,
  284. 'qx' : c,
  285. 'qy' : r
  286. };
  287. tileCol.push(tile);
  288. }
  289. this.tiles.push(tileCol);
  290. }
  291. this.surface.onmousedown = PanoJS.mousePressedHandler;
  292. this.surface.onmouseup = this.surface.onmouseout = PanoJS.mouseReleasedHandler;
  293. this.surface.ondblclick = PanoJS.doubleClickHandler;
  294. if (PanoJS.USE_KEYBOARD) {
  295. window.onkeypress = PanoJS.keyboardMoveHandler;
  296. window.onkeydown = PanoJS.keyboardZoomHandler;
  297. }
  298. this.positionTiles();
  299. },
  300. /**
  301. * Position the tiles based on the x, y coordinates of the
  302. * viewer, taking into account the motion offsets, which
  303. * are calculated by a motion event handler.
  304. */
  305. positionTiles : function(motion, reset) {
  306. // default to no motion, just setup tiles
  307. if (typeof motion == 'undefined') {
  308. motion = { 'x' : 0, 'y' : 0 };
  309. }
  310. for (var c = 0; c < this.tiles.length; c++) {
  311. for (var r = 0; r < this.tiles[c].length; r++) {
  312. var tile = this.tiles[c][r];
  313. tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
  314. tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
  315. var visible = true;
  316. if (tile.posx > this.width) {
  317. // tile moved out of view to the right
  318. // consider the tile coming into view from the left
  319. do {
  320. tile.xIndex -= this.tiles.length;
  321. tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
  322. } while (tile.posx > this.width);
  323. if (tile.posx + this.tileSize < 0) {
  324. visible = false;
  325. }
  326. } else {
  327. // tile may have moved out of view from the left
  328. // if so, consider the tile coming into view from the right
  329. while (tile.posx < -this.tileSize) {
  330. tile.xIndex += this.tiles.length;
  331. tile.posx = (tile.xIndex * this.tileSize) + this.x + motion.x;
  332. }
  333. if (tile.posx > this.width) {
  334. visible = false;
  335. }
  336. }
  337. if (tile.posy > this.height) {
  338. // tile moved out of view to the bottom
  339. // consider the tile coming into view from the top
  340. do {
  341. tile.yIndex -= this.tiles[c].length;
  342. tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
  343. } while (tile.posy > this.height);
  344. if (tile.posy + this.tileSize < 0) {
  345. visible = false;
  346. }
  347. } else {
  348. // tile may have moved out of view to the top
  349. // if so, consider the tile coming into view from the bottom
  350. while (tile.posy < -this.tileSize) {
  351. tile.yIndex += this.tiles[c].length;
  352. tile.posy = (tile.yIndex * this.tileSize) + this.y + motion.y;
  353. }
  354. if (tile.posy > this.height) {
  355. visible = false;
  356. }
  357. }
  358. // initialize the image object for this quadrant
  359. if (!this.initialized) {
  360. this.assignTileImage(tile, true);
  361. tile.element.style.top = tile.posy + 'px';
  362. tile.element.style.left = tile.posx + 'px';
  363. }
  364. // display the image if visible
  365. if (visible) {
  366. this.assignTileImage(tile);
  367. }
  368. // seems to need this no matter what
  369. tile.element.style.top = tile.posy + 'px';
  370. tile.element.style.left = tile.posx + 'px';
  371. }
  372. }
  373. // reset the x, y coordinates of the viewer according to motion
  374. if (reset) {
  375. this.x += motion.x;
  376. this.y += motion.y;
  377. }
  378. },
  379. /**
  380. * Determine the source image of the specified tile based
  381. * on the zoom level and position of the tile. If forceBlankImage
  382. * is specified, the source should be automatically set to the
  383. * null tile image. This method will also setup an onload
  384. * routine, delaying the appearance of the tile until it is fully
  385. * loaded, if configured to do so.
  386. */
  387. assignTileImage : function(tile, forceBlankImage) {
  388. var tileImgId, src;
  389. var useBlankImage = (forceBlankImage ? true : false);
  390. // check if image has been scrolled too far in any particular direction
  391. // and if so, use the null tile image
  392. if (!useBlankImage) {
  393. var left = tile.xIndex < 0;
  394. var high = tile.yIndex < 0;
  395. var right = tile.xIndex >= Math.pow(2, this.zoomLevel);
  396. var low = tile.yIndex >= Math.pow(2, this.zoomLevel);
  397. if (high || left || low || right) {
  398. useBlankImage = true;
  399. }
  400. }
  401. if (useBlankImage) {
  402. tileImgId = 'blank:' + tile.qx + ':' + tile.qy;
  403. src = this.cache['blank'].src;
  404. }
  405. else {
  406. tileImgId = src = this.tileUrlProvider.assembleUrl(tile.xIndex, tile.yIndex, this.zoomLevel);
  407. }
  408. // only remove tile if identity is changing
  409. if (tile.element != null &&
  410. tile.element.parentNode != null &&
  411. tile.element.relativeSrc != src) {
  412. this.well.removeChild(tile.element);
  413. }
  414. var tileImg = this.cache[tileImgId];
  415. // create cache if not exist
  416. if (tileImg == null) {
  417. tileImg = this.cache[tileImgId] = this.createPrototype(src);
  418. }
  419. if (useBlankImage || !PanoJS.USE_LOADER_IMAGE || tileImg.complete || (tileImg.image && tileImg.image.complete)) {
  420. tileImg.onload = function() {};
  421. if (tileImg.image) {
  422. tileImg.image.onload = function() {};
  423. }
  424. if (tileImg.parentNode == null) {
  425. tile.element = this.well.appendChild(tileImg);
  426. }
  427. }
  428. else {
  429. var loadingImgId = 'loading:' + tile.qx + ':' + tile.qy;
  430. var loadingImg = this.cache[loadingImgId];
  431. if (loadingImg == null) {
  432. loadingImg = this.cache[loadingImgId] = this.createPrototype(this.cache['loading'].src);
  433. }
  434. loadingImg.targetSrc = tileImgId;
  435. var well = this.well;
  436. tile.element = well.appendChild(loadingImg);
  437. tileImg.onload = function() {
  438. // make sure our destination is still present
  439. if (loadingImg.parentNode && loadingImg.targetSrc == tileImgId) {
  440. tileImg.style.top = loadingImg.style.top;
  441. tileImg.style.left = loadingImg.style.left;
  442. well.replaceChild(tileImg, loadingImg);
  443. tile.element = tileImg;
  444. }
  445. tileImg.onload = function() {};
  446. return false;
  447. }
  448. // konqueror only recognizes the onload event on an Image
  449. // javascript object, so we must handle that case here
  450. if (!PanoJS.DOM_ONLOAD) {
  451. tileImg.image = new Image();
  452. tileImg.image.onload = tileImg.onload;
  453. tileImg.image.src = tileImg.src;
  454. }
  455. }
  456. },
  457. createPrototype : function(src) {
  458. var img = document.createElement('img');
  459. img.src = src;
  460. img.relativeSrc = src;
  461. img.className = PanoJS.TILE_STYLE_CLASS;
  462. img.style.width = this.tileSize + 'px';
  463. img.style.height = this.tileSize + 'px';
  464. return img;
  465. },
  466. addViewerMovedListener : function(listener) {
  467. this.viewerMovedListeners.push(listener);
  468. },
  469. addViewerZoomedListener : function(listener) {
  470. this.viewerZoomedListeners.push(listener);
  471. },
  472. /**
  473. * Notify listeners of a zoom event on the viewer.
  474. */
  475. notifyViewerZoomed : function() {
  476. var percentage = (100/(this.maxZoomLevel + 1)) * (this.zoomLevel + 1);
  477. for (var i = 0; i < this.viewerZoomedListeners.length; i++) {
  478. this.viewerZoomedListeners[i].viewerZoomed(
  479. new PanoJS.ZoomEvent(this.x, this.y, this.zoomLevel, percentage)
  480. );
  481. }
  482. },
  483. /**
  484. * Notify listeners of a move event on the viewer.
  485. */
  486. notifyViewerMoved : function(coords) {
  487. if (typeof coords == 'undefined') {
  488. coords = { 'x' : 0, 'y' : 0 };
  489. }
  490. for (var i = 0; i < this.viewerMovedListeners.length; i++) {
  491. this.viewerMovedListeners[i].viewerMoved(
  492. new PanoJS.MoveEvent(
  493. this.x + (coords.x - this.mark.x),
  494. this.y + (coords.y - this.mark.y)
  495. )
  496. );
  497. }
  498. },
  499. zoom : function(direction) {
  500. // ensure we are not zooming out of range
  501. if (this.zoomLevel + direction < 0) {
  502. if (PanoJS.MSG_BEYOND_MIN_ZOOM) {
  503. alert(PanoJS.MSG_BEYOND_MIN_ZOOM);
  504. }
  505. return;
  506. }
  507. else if (this.zoomLevel + direction > this.maxZoomLevel) {
  508. if (PanoJS.MSG_BEYOND_MAX_ZOOM) {
  509. alert(PanoJS.MSG_BEYOND_MAX_ZOOM);
  510. }
  511. return;
  512. }
  513. this.blank();
  514. var coords = { 'x' : Math.floor(this.width / 2), 'y' : Math.floor(this.height / 2) };
  515. var before = {
  516. 'x' : (coords.x - this.x),
  517. 'y' : (coords.y - this.y)
  518. };
  519. var after = {
  520. 'x' : Math.floor(before.x * Math.pow(2, direction)),
  521. 'y' : Math.floor(before.y * Math.pow(2, direction))
  522. };
  523. this.x = coords.x - after.x;
  524. this.y = coords.y - after.y;
  525. this.zoomLevel += direction;
  526. this.positionTiles();
  527. this.notifyViewerZoomed();
  528. },
  529. /**
  530. * Clear all the tiles from the well for a complete reinitialization of the
  531. * viewer. At this point the viewer is not considered to be initialized.
  532. */
  533. clear : function() {
  534. this.blank();
  535. this.initialized = false;
  536. this.tiles = [];
  537. },
  538. /**
  539. * Remove all tiles from the well, which effectively "hides"
  540. * them for a repaint.
  541. */
  542. blank : function() {
  543. for (imgId in this.cache) {
  544. var img = this.cache[imgId];
  545. img.onload = function() {};
  546. if (img.image) {
  547. img.image.onload = function() {};
  548. }
  549. if (img.parentNode != null) {
  550. this.well.removeChild(img);
  551. }
  552. }
  553. },
  554. /**
  555. * Method specifically for handling a mouse move event. A direct
  556. * movement of the viewer can be achieved by calling positionTiles() directly.
  557. */
  558. moveViewer : function(coords) {
  559. this.positionTiles({ 'x' : (coords.x - this.mark.x), 'y' : (coords.y - this.mark.y) });
  560. this.notifyViewerMoved(coords);
  561. },
  562. /**
  563. * Make the specified coords the new center of the image placement.
  564. * This method is typically triggered as the result of a double-click
  565. * event. The calculation considers the distance between the center
  566. * of the viewable area and the specified (viewer-relative) coordinates.
  567. * If absolute is specified, treat the point as relative to the entire
  568. * image, rather than only the viewable portion.
  569. */
  570. recenter : function(coords, absolute) {
  571. if (absolute) {
  572. coords.x += this.x;
  573. coords.y += this.y;
  574. }
  575. var motion = {
  576. 'x' : Math.floor((this.width / 2) - coords.x),
  577. 'y' : Math.floor((this.height / 2) - coords.y)
  578. };
  579. if (motion.x == 0 && motion.y == 0) {
  580. return;
  581. }
  582. if (PanoJS.USE_SLIDE) {
  583. var target = motion;
  584. var x, y;
  585. // handle special case of vertical movement
  586. if (target.x == 0) {
  587. x = 0;
  588. y = this.slideAcceleration;
  589. }
  590. else {
  591. var slope = Math.abs(target.y / target.x);
  592. x = Math.round(Math.pow(Math.pow(this.slideAcceleration, 2) / (1 + Math.pow(slope, 2)), .5));
  593. y = Math.round(slope * x);
  594. }
  595. motion = {
  596. 'x' : Math.min(x, Math.abs(target.x)) * (target.x < 0 ? -1 : 1),
  597. 'y' : Math.min(y, Math.abs(target.y)) * (target.y < 0 ? -1 : 1)
  598. }
  599. }
  600. this.positionTiles(motion, true);
  601. this.notifyViewerMoved();
  602. if (!PanoJS.USE_SLIDE) {
  603. return;
  604. }
  605. var newcoords = {
  606. 'x' : coords.x + motion.x,
  607. 'y' : coords.y + motion.y
  608. };
  609. var self = this;
  610. // TODO: use an exponential growth rather than linear (should also depend on how far we are going)
  611. // FIXME: this could be optimized by calling positionTiles directly perhaps
  612. this.slideAcceleration += PanoJS.SLIDE_ACCELERATION_FACTOR;
  613. this.slideMonitor = setTimeout(function() { self.recenter(newcoords); }, PanoJS.SLIDE_DELAY );
  614. },
  615. resize : function() {
  616. // IE fires a premature resize event
  617. if (!this.initialized) {
  618. return;
  619. }
  620. var newWidth = this.viewer.offsetWidth;
  621. var newHeight = this.viewer.offsetHeight;
  622. this.viewer.style.display = 'none';
  623. this.clear();
  624. var before = {
  625. 'x' : Math.floor(this.width / 2),
  626. 'y' : Math.floor(this.height / 2)
  627. };
  628. if (this.border >= 0) {
  629. this.fitToWindow(this.border);
  630. }
  631. else {
  632. this.width = newWidth;
  633. this.height = newHeight;
  634. }
  635. this.prepareTiles();
  636. var after = {
  637. 'x' : Math.floor(this.width / 2),
  638. 'y' : Math.floor(this.height / 2)
  639. };
  640. if (this.border >= 0) {
  641. this.x += (after.x - before.x);
  642. this.y += (after.y - before.y);
  643. }
  644. this.positionTiles();
  645. this.viewer.style.display = '';
  646. this.initialized = true;
  647. this.notifyViewerMoved();
  648. },
  649. /**
  650. * Resolve the coordinates from this mouse event by subtracting the
  651. * offset of the viewer in the browser window (or frame). This does
  652. * take into account the scroll offset of the page.
  653. */
  654. resolveCoordinates : function(e) {
  655. return {
  656. 'x' : (e.pageX || (e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft))) - this.left,
  657. 'y' : (e.pageY || (e.clientY + (document.documentElement.scrollTop || document.body.scrollTop))) - this.top
  658. }
  659. },
  660. press : function(coords) {
  661. this.activate(true);
  662. this.mark = coords;
  663. },
  664. release : function(coords) {
  665. this.activate(false);
  666. var motion = {
  667. 'x' : (coords.x - this.mark.x),
  668. 'y' : (coords.y - this.mark.y)
  669. };
  670. this.x += motion.x;
  671. this.y += motion.y;
  672. this.mark = { 'x' : 0, 'y' : 0 };
  673. },
  674. /**
  675. * Activate the viewer into motion depending on whether the mouse is pressed or
  676. * not pressed. This method localizes the changes that must be made to the
  677. * layers.
  678. */
  679. activate : function(pressed) {
  680. this.pressed = pressed;
  681. this.surface.style.cursor = (pressed ? PanoJS.GRABBING_MOUSE_CURSOR : PanoJS.GRAB_MOUSE_CURSOR);
  682. this.surface.onmousemove = (pressed ? PanoJS.mouseMovedHandler : function() {});
  683. },
  684. /**
  685. * Check whether the specified point exceeds the boundaries of
  686. * the viewer's primary image.
  687. */
  688. pointExceedsBoundaries : function(coords) {
  689. return (coords.x < this.x ||
  690. coords.y < this.y ||
  691. coords.x > (this.tileSize * Math.pow(2, this.zoomLevel) + this.x) ||
  692. coords.y > (this.tileSize * Math.pow(2, this.zoomLevel) + this.y));
  693. },
  694. // QUESTION: where is the best place for this method to be invoked?
  695. resetSlideMotion : function() {
  696. // QUESTION: should this be > 0 ?
  697. if (this.slideMonitor != 0) {
  698. clearTimeout(this.slideMonitor);
  699. this.slideMonitor = 0;
  700. }
  701. this.slideAcceleration = 0;
  702. }
  703. };
  704. PanoJS.TileUrlProvider = function(baseUri, prefix, extension) {
  705. this.baseUri = baseUri;
  706. this.prefix = prefix;
  707. this.extension = extension;
  708. }
  709. PanoJS.TileUrlProvider.prototype = {
  710. assembleUrl: function(xIndex, yIndex, zoom) {
  711. return this.baseUri + '/' +
  712. this.prefix + zoom + '-' + xIndex + '-' + yIndex + '.' + this.extension +
  713. (PanoJS.REVISION_FLAG ? '?r=' + PanoJS.REVISION_FLAG : '');
  714. }
  715. }
  716. PanoJS.mousePressedHandler = function(e) {
  717. e = e ? e : window.event;
  718. // only grab on left-click
  719. if (e.button < 2) {
  720. var self = this.backingBean;
  721. var coords = self.resolveCoordinates(e);
  722. if (self.pointExceedsBoundaries(coords)) {
  723. e.cancelBubble = true;
  724. }
  725. else {
  726. self.press(coords);
  727. }
  728. }
  729. // NOTE: MANDATORY! must return false so event does not propagate to well!
  730. return false;
  731. };
  732. PanoJS.mouseReleasedHandler = function(e) {
  733. e = e ? e : window.event;
  734. var self = this.backingBean;
  735. if (self.pressed) {
  736. // OPTION: could decide to move viewer only on release, right here
  737. self.release(self.resolveCoordinates(e));
  738. }
  739. };
  740. PanoJS.mouseMovedHandler = function(e) {
  741. e = e ? e : window.event;
  742. var self = this.backingBean;
  743. self.moveCount++;
  744. if (self.moveCount % PanoJS.MOVE_THROTTLE == 0) {
  745. self.moveViewer(self.resolveCoordinates(e));
  746. }
  747. };
  748. PanoJS.zoomInHandler = function(e) {
  749. e = e ? e : window.event;
  750. var self = this.parentNode.parentNode.backingBean;
  751. self.zoom(1);
  752. return false;
  753. };
  754. PanoJS.zoomOutHandler = function(e) {
  755. e = e ? e : window.event;
  756. var self = this.parentNode.parentNode.backingBean;
  757. self.zoom(-1);
  758. return false;
  759. };
  760. PanoJS.doubleClickHandler = function(e) {
  761. e = e ? e : window.event;
  762. var self = this.backingBean;
  763. coords = self.resolveCoordinates(e);
  764. if (!self.pointExceedsBoundaries(coords)) {
  765. self.resetSlideMotion();
  766. self.recenter(coords);
  767. }
  768. };
  769. PanoJS.keyboardMoveHandler = function(e) {
  770. e = e ? e : window.event;
  771. for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
  772. var viewer = PanoJS.VIEWERS[i];
  773. if (e.keyCode == 38)
  774. viewer.positionTiles({'x': 0,'y': -PanoJS.MOVE_THROTTLE}, true);
  775. if (e.keyCode == 39)
  776. viewer.positionTiles({'x': -PanoJS.MOVE_THROTTLE,'y': 0}, true);
  777. if (e.keyCode == 40)
  778. viewer.positionTiles({'x': 0,'y': PanoJS.MOVE_THROTTLE}, true);
  779. if (e.keyCode == 37)
  780. viewer.positionTiles({'x': PanoJS.MOVE_THROTTLE,'y': 0}, true);
  781. }
  782. }
  783. PanoJS.keyboardZoomHandler = function(e) {
  784. e = e ? e : window.event;
  785. for (var i = 0; i < PanoJS.VIEWERS.length; i++) {
  786. var viewer = PanoJS.VIEWERS[i];
  787. if (e.keyCode == 109)
  788. viewer.zoom(-1);
  789. if (e.keyCode == 107)
  790. viewer.zoom(1);
  791. }
  792. }
  793. PanoJS.MoveEvent = function(x, y) {
  794. this.x = x;
  795. this.y = y;
  796. };
  797. PanoJS.ZoomEvent = function(x, y, level, percentage) {
  798. this.x = x;
  799. this.y = y;
  800. this.percentage = percentage;
  801. this.level = level;
  802. };