Image.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. /* Copyright (c) 2006-2011 by OpenLayers Contributors (see authors.txt for
  2. * full list of contributors). Published under the Clear BSD license.
  3. * See http://svn.openlayers.org/trunk/openlayers/license.txt for the
  4. * full text of the license. */
  5. /**
  6. * @requires OpenLayers/Tile.js
  7. */
  8. /**
  9. * Class: OpenLayers.Tile.Image
  10. * Instances of OpenLayers.Tile.Image are used to manage the image tiles
  11. * used by various layers. Create a new image tile with the
  12. * <OpenLayers.Tile.Image> constructor.
  13. *
  14. * Inherits from:
  15. * - <OpenLayers.Tile>
  16. */
  17. OpenLayers.Tile.Image = OpenLayers.Class(OpenLayers.Tile, {
  18. /**
  19. * Property: url
  20. * {String} The URL of the image being requested. No default. Filled in by
  21. * layer.getURL() function.
  22. */
  23. url: null,
  24. /**
  25. * Property: imgDiv
  26. * {DOMElement} The div element which wraps the image.
  27. */
  28. imgDiv: null,
  29. /**
  30. * Property: frame
  31. * {DOMElement} The image element is appended to the frame. Any gutter on
  32. * the image will be hidden behind the frame.
  33. */
  34. frame: null,
  35. /**
  36. * Property: layerAlphaHack
  37. * {Boolean} True if the png alpha hack needs to be applied on the layer's div.
  38. */
  39. layerAlphaHack: null,
  40. /**
  41. * Property: isBackBuffer
  42. * {Boolean} Is this tile a back buffer tile?
  43. */
  44. isBackBuffer: false,
  45. /**
  46. * Property: isFirstDraw
  47. * {Boolean} Is this the first time the tile is being drawn?
  48. * This is used to force resetBackBuffer to synchronize
  49. * the backBufferTile with the foreground tile the first time
  50. * the foreground tile loads so that if the user zooms
  51. * before the layer has fully loaded, the backBufferTile for
  52. * tiles that have been loaded can be used.
  53. */
  54. isFirstDraw: true,
  55. /**
  56. * Property: backBufferTile
  57. * {<OpenLayers.Tile>} A clone of the tile used to create transition
  58. * effects when the tile is moved or changes resolution.
  59. */
  60. backBufferTile: null,
  61. /**
  62. * APIProperty: maxGetUrlLength
  63. * {Number} If set, requests that would result in GET urls with more
  64. * characters than the number provided will be made using form-encoded
  65. * HTTP POST. It is good practice to avoid urls that are longer than 2048
  66. * characters.
  67. *
  68. * Caution:
  69. * Older versions of Gecko based browsers (e.g. Firefox < 3.5) and
  70. * Opera < 10.0 do not fully support this option.
  71. *
  72. * Note:
  73. * Do not use this option for layers that have a transitionEffect
  74. * configured - IFrame tiles from POST requests can not be resized.
  75. */
  76. maxGetUrlLength: null,
  77. /** TBD 3.0 - reorder the parameters to the init function to remove
  78. * URL. the getUrl() function on the layer gets called on
  79. * each draw(), so no need to specify it here.
  80. *
  81. * Constructor: OpenLayers.Tile.Image
  82. * Constructor for a new <OpenLayers.Tile.Image> instance.
  83. *
  84. * Parameters:
  85. * layer - {<OpenLayers.Layer>} layer that the tile will go in.
  86. * position - {<OpenLayers.Pixel>}
  87. * bounds - {<OpenLayers.Bounds>}
  88. * url - {<String>} Deprecated. Remove me in 3.0.
  89. * size - {<OpenLayers.Size>}
  90. * options - {Object}
  91. */
  92. initialize: function(layer, position, bounds, url, size, options) {
  93. OpenLayers.Tile.prototype.initialize.apply(this, arguments);
  94. if (this.maxGetUrlLength != null) {
  95. OpenLayers.Util.extend(this, OpenLayers.Tile.Image.IFrame);
  96. }
  97. this.url = url; //deprecated remove me
  98. this.frame = document.createElement('div');
  99. this.frame.style.overflow = 'hidden';
  100. this.frame.style.position = 'absolute';
  101. this.layerAlphaHack = this.layer.alpha && OpenLayers.Util.alphaHack();
  102. },
  103. /**
  104. * APIMethod: destroy
  105. * nullify references to prevent circular references and memory leaks
  106. */
  107. destroy: function() {
  108. if (this.imgDiv != null) {
  109. this.removeImgDiv();
  110. }
  111. this.imgDiv = null;
  112. if ((this.frame != null) && (this.frame.parentNode == this.layer.div)) {
  113. this.layer.div.removeChild(this.frame);
  114. }
  115. this.frame = null;
  116. /* clean up the backBufferTile if it exists */
  117. if (this.backBufferTile) {
  118. this.backBufferTile.destroy();
  119. this.backBufferTile = null;
  120. }
  121. this.layer.events.unregister("loadend", this, this.resetBackBuffer);
  122. OpenLayers.Tile.prototype.destroy.apply(this, arguments);
  123. },
  124. /**
  125. * Method: clone
  126. *
  127. * Parameters:
  128. * obj - {<OpenLayers.Tile.Image>} The tile to be cloned
  129. *
  130. * Returns:
  131. * {<OpenLayers.Tile.Image>} An exact clone of this <OpenLayers.Tile.Image>
  132. */
  133. clone: function (obj) {
  134. if (obj == null) {
  135. obj = new OpenLayers.Tile.Image(this.layer,
  136. this.position,
  137. this.bounds,
  138. this.url,
  139. this.size);
  140. }
  141. //pick up properties from superclass
  142. obj = OpenLayers.Tile.prototype.clone.apply(this, [obj]);
  143. //dont want to directly copy the image div
  144. obj.imgDiv = null;
  145. return obj;
  146. },
  147. /**
  148. * Method: draw
  149. * Check that a tile should be drawn, and draw it.
  150. *
  151. * Returns:
  152. * {Boolean} Always returns true.
  153. */
  154. draw: function() {
  155. if (this.layer != this.layer.map.baseLayer && this.layer.reproject) {
  156. this.bounds = this.getBoundsFromBaseLayer(this.position);
  157. }
  158. var drawTile = OpenLayers.Tile.prototype.draw.apply(this, arguments);
  159. if ((OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS, this.layer.transitionEffect) != -1) ||
  160. this.layer.singleTile) {
  161. if (drawTile) {
  162. //we use a clone of this tile to create a double buffer for visual
  163. //continuity. The backBufferTile is used to create transition
  164. //effects while the tile in the grid is repositioned and redrawn
  165. if (!this.backBufferTile) {
  166. this.backBufferTile = this.clone();
  167. this.backBufferTile.hide();
  168. // this is important. It allows the backBuffer to place itself
  169. // appropriately in the DOM. The Image subclass needs to put
  170. // the backBufferTile behind the main tile so the tiles can
  171. // load over top and display as soon as they are loaded.
  172. this.backBufferTile.isBackBuffer = true;
  173. // potentially end any transition effects when the tile loads
  174. this.events.register('loadend', this, this.resetBackBuffer);
  175. // clear transition back buffer tile only after all tiles in
  176. // this layer have loaded to avoid visual glitches
  177. this.layer.events.register("loadend", this, this.resetBackBuffer);
  178. }
  179. // run any transition effects
  180. this.startTransition();
  181. } else {
  182. // if we aren't going to draw the tile, then the backBuffer should
  183. // be hidden too!
  184. if (this.backBufferTile) {
  185. this.backBufferTile.clear();
  186. }
  187. }
  188. } else {
  189. if (drawTile && this.isFirstDraw) {
  190. this.events.register('loadend', this, this.showTile);
  191. this.isFirstDraw = false;
  192. }
  193. }
  194. if (!drawTile) {
  195. return false;
  196. }
  197. if (this.isLoading) {
  198. //if we're already loading, send 'reload' instead of 'loadstart'.
  199. this.events.triggerEvent("reload");
  200. } else {
  201. this.isLoading = true;
  202. this.events.triggerEvent("loadstart");
  203. }
  204. return this.renderTile();
  205. },
  206. /**
  207. * Method: resetBackBuffer
  208. * Triggered by two different events, layer loadend, and tile loadend.
  209. * In any of these cases, we check to see if we can hide the
  210. * backBufferTile yet and update its parameters to match the
  211. * foreground tile.
  212. *
  213. * Basic logic:
  214. * - If the backBufferTile hasn't been drawn yet, reset it
  215. * - If layer is still loading, show foreground tile but don't hide
  216. * the backBufferTile yet
  217. * - If layer is done loading, reset backBuffer tile and show
  218. * foreground tile
  219. */
  220. resetBackBuffer: function() {
  221. this.showTile();
  222. if (this.backBufferTile &&
  223. (this.isFirstDraw || !this.layer.numLoadingTiles)) {
  224. this.isFirstDraw = false;
  225. // check to see if the backBufferTile is within the max extents
  226. // before rendering it
  227. var maxExtent = this.layer.maxExtent;
  228. var withinMaxExtent = (maxExtent &&
  229. this.bounds.intersectsBounds(maxExtent, false));
  230. if (withinMaxExtent) {
  231. this.backBufferTile.position = this.position;
  232. this.backBufferTile.bounds = this.bounds;
  233. this.backBufferTile.size = this.size;
  234. this.backBufferTile.imageSize = this.layer.getImageSize(this.bounds) || this.size;
  235. this.backBufferTile.imageOffset = this.layer.imageOffset;
  236. this.backBufferTile.resolution = this.layer.getResolution();
  237. this.backBufferTile.renderTile();
  238. }
  239. this.backBufferTile.hide();
  240. }
  241. },
  242. /**
  243. * Method: renderTile
  244. * Internal function to actually initialize the image tile,
  245. * position it correctly, and set its url.
  246. */
  247. renderTile: function() {
  248. if (this.layer.async) {
  249. this.initImgDiv();
  250. // Asyncronous image requests call the asynchronous getURL method
  251. // on the layer to fetch an image that covers 'this.bounds', in the scope of
  252. // 'this', setting the 'url' property of the layer itself, and running
  253. // the callback 'positionFrame' when the image request returns.
  254. this.layer.getURLasync(this.bounds, this, "url", this.positionImage);
  255. } else {
  256. // syncronous image requests get the url and position the frame immediately,
  257. // and don't wait for an image request to come back.
  258. this.url = this.layer.getURL(this.bounds);
  259. this.initImgDiv();
  260. // position the frame immediately
  261. this.positionImage();
  262. }
  263. return true;
  264. },
  265. /**
  266. * Method: positionImage
  267. * Using the properties currenty set on the layer, position the tile correctly.
  268. * This method is used both by the async and non-async versions of the Tile.Image
  269. * code.
  270. */
  271. positionImage: function() {
  272. // if the this layer doesn't exist at the point the image is
  273. // returned, do not attempt to use it for size computation
  274. if (this.layer === null) {
  275. return;
  276. }
  277. // position the frame
  278. OpenLayers.Util.modifyDOMElement(this.frame,
  279. null, this.position, this.size);
  280. var imageSize = this.layer.getImageSize(this.bounds);
  281. if (this.layerAlphaHack) {
  282. OpenLayers.Util.modifyAlphaImageDiv(this.imgDiv,
  283. null, null, imageSize, this.url);
  284. } else {
  285. OpenLayers.Util.modifyDOMElement(this.imgDiv,
  286. null, null, imageSize) ;
  287. this.imgDiv.src = this.url;
  288. }
  289. },
  290. /**
  291. * Method: clear
  292. * Clear the tile of any bounds/position-related data so that it can
  293. * be reused in a new location.
  294. */
  295. clear: function() {
  296. if(this.imgDiv) {
  297. this.hide();
  298. if (OpenLayers.Tile.Image.useBlankTile) {
  299. this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif";
  300. }
  301. }
  302. },
  303. /**
  304. * Method: initImgDiv
  305. * Creates the imgDiv property on the tile.
  306. */
  307. initImgDiv: function() {
  308. if (this.imgDiv == null) {
  309. var offset = this.layer.imageOffset;
  310. var size = this.layer.getImageSize(this.bounds);
  311. if (this.layerAlphaHack) {
  312. this.imgDiv = OpenLayers.Util.createAlphaImageDiv(null,
  313. offset,
  314. size,
  315. null,
  316. "relative",
  317. null,
  318. null,
  319. null,
  320. true);
  321. } else {
  322. this.imgDiv = OpenLayers.Util.createImage(null,
  323. offset,
  324. size,
  325. null,
  326. "relative",
  327. null,
  328. null,
  329. true);
  330. }
  331. // needed for changing to a different server for onload error
  332. if (OpenLayers.Util.isArray(this.layer.url)) {
  333. this.imgDiv.urls = this.layer.url.slice();
  334. }
  335. this.imgDiv.className = 'olTileImage';
  336. /* checkImgURL used to be used to called as a work around, but it
  337. ended up hiding problems instead of solving them and broke things
  338. like relative URLs. See discussion on the dev list:
  339. http://openlayers.org/pipermail/dev/2007-January/000205.html
  340. OpenLayers.Event.observe( this.imgDiv, "load",
  341. OpenLayers.Function.bind(this.checkImgURL, this) );
  342. */
  343. this.frame.style.zIndex = this.isBackBuffer ? 0 : 1;
  344. this.frame.appendChild(this.imgDiv);
  345. this.layer.div.appendChild(this.frame);
  346. if(this.layer.opacity != null) {
  347. OpenLayers.Util.modifyDOMElement(this.imgDiv, null, null, null,
  348. null, null, null,
  349. this.layer.opacity);
  350. }
  351. // we need this reference to check back the viewRequestID
  352. this.imgDiv.map = this.layer.map;
  353. //bind a listener to the onload of the image div so that we
  354. // can register when a tile has finished loading.
  355. var onload = function() {
  356. //normally isLoading should always be true here but there are some
  357. // right funky conditions where loading and then reloading a tile
  358. // with the same url *really*fast*. this check prevents sending
  359. // a 'loadend' if the msg has already been sent
  360. //
  361. if (this.isLoading) {
  362. this.isLoading = false;
  363. this.events.triggerEvent("loadend");
  364. }
  365. };
  366. if (this.layerAlphaHack) {
  367. OpenLayers.Event.observe(this.imgDiv.childNodes[0], 'load',
  368. OpenLayers.Function.bind(onload, this));
  369. } else {
  370. OpenLayers.Event.observe(this.imgDiv, 'load',
  371. OpenLayers.Function.bind(onload, this));
  372. }
  373. // Bind a listener to the onerror of the image div so that we
  374. // can registere when a tile has finished loading with errors.
  375. var onerror = function() {
  376. // If we have gone through all image reload attempts, it is time
  377. // to realize that we are done with this image. Since
  378. // OpenLayers.Util.onImageLoadError already has taken care about
  379. // the error, we can continue as if the image was loaded
  380. // successfully.
  381. if (this.imgDiv._attempts > OpenLayers.IMAGE_RELOAD_ATTEMPTS) {
  382. onload.call(this);
  383. }
  384. };
  385. OpenLayers.Event.observe(this.imgDiv, "error",
  386. OpenLayers.Function.bind(onerror, this));
  387. }
  388. this.imgDiv.viewRequestID = this.layer.map.viewRequestID;
  389. },
  390. /**
  391. * Method: removeImgDiv
  392. * Removes the imgDiv from the DOM and stops listening to events on it.
  393. */
  394. removeImgDiv: function() {
  395. // unregister the "load" and "error" handlers. Only the "error" handler if
  396. // this.layerAlphaHack is true.
  397. OpenLayers.Event.stopObservingElement(this.imgDiv);
  398. if (this.imgDiv.parentNode == this.frame) {
  399. this.frame.removeChild(this.imgDiv);
  400. this.imgDiv.map = null;
  401. }
  402. this.imgDiv.urls = null;
  403. var child = this.imgDiv.firstChild;
  404. //check for children (alphaHack img or IFrame)
  405. if (child) {
  406. OpenLayers.Event.stopObservingElement(child);
  407. this.imgDiv.removeChild(child);
  408. delete child;
  409. } else {
  410. // abort any currently loading image
  411. this.imgDiv.src = OpenLayers.Util.getImagesLocation() + "blank.gif";
  412. }
  413. },
  414. /**
  415. * Method: checkImgURL
  416. * Make sure that the image that just loaded is the one this tile is meant
  417. * to display, since panning/zooming might have changed the tile's URL in
  418. * the meantime. If the tile URL did change before the image loaded, set
  419. * the imgDiv display to 'none', as either (a) it will be reset to visible
  420. * when the new URL loads in the image, or (b) we don't want to display
  421. * this tile after all because its new bounds are outside our maxExtent.
  422. *
  423. * This function should no longer be neccesary with the improvements to
  424. * Grid.js in OpenLayers 2.3. The lack of a good isEquivilantURL function
  425. * caused problems in 2.2, but it's possible that with the improved
  426. * isEquivilant URL function, this might be neccesary at some point.
  427. *
  428. * See discussion in the thread at
  429. * http://openlayers.org/pipermail/dev/2007-January/000205.html
  430. */
  431. checkImgURL: function () {
  432. // Sometimes our image will load after it has already been removed
  433. // from the map, in which case this check is not needed.
  434. if (this.layer) {
  435. var loaded = this.layerAlphaHack ? this.imgDiv.firstChild.src : this.imgDiv.src;
  436. if (!OpenLayers.Util.isEquivalentUrl(loaded, this.url)) {
  437. this.hide();
  438. }
  439. }
  440. },
  441. /**
  442. * Method: startTransition
  443. * This method is invoked on tiles that are backBuffers for tiles in the
  444. * grid. The grid tile is about to be cleared and a new tile source
  445. * loaded. This is where the transition effect needs to be started
  446. * to provide visual continuity.
  447. */
  448. startTransition: function() {
  449. // backBufferTile has to be valid and ready to use
  450. if (!this.backBufferTile || !this.backBufferTile.imgDiv) {
  451. return;
  452. }
  453. // calculate the ratio of change between the current resolution of the
  454. // backBufferTile and the layer. If several animations happen in a
  455. // row, then the backBufferTile will scale itself appropriately for
  456. // each request.
  457. var ratio = 1;
  458. if (this.backBufferTile.resolution) {
  459. ratio = this.backBufferTile.resolution / this.layer.getResolution();
  460. }
  461. // if the ratio is not the same as it was last time (i.e. we are
  462. // zooming), then we need to adjust the backBuffer tile
  463. if (ratio != 1) {
  464. if (this.layer.transitionEffect == 'resize') {
  465. // In this case, we can just immediately resize the
  466. // backBufferTile.
  467. var upperLeft = new OpenLayers.LonLat(
  468. this.backBufferTile.bounds.left,
  469. this.backBufferTile.bounds.top
  470. );
  471. var size = new OpenLayers.Size(
  472. this.backBufferTile.size.w * ratio,
  473. this.backBufferTile.size.h * ratio
  474. );
  475. var px = this.layer.map.getLayerPxFromLonLat(upperLeft);
  476. OpenLayers.Util.modifyDOMElement(this.backBufferTile.frame,
  477. null, px, size);
  478. var imageSize = this.backBufferTile.imageSize;
  479. imageSize = new OpenLayers.Size(imageSize.w * ratio,
  480. imageSize.h * ratio);
  481. var imageOffset = this.backBufferTile.imageOffset;
  482. if(imageOffset) {
  483. imageOffset = new OpenLayers.Pixel(
  484. imageOffset.x * ratio, imageOffset.y * ratio
  485. );
  486. }
  487. OpenLayers.Util.modifyDOMElement(
  488. this.backBufferTile.imgDiv, null, imageOffset, imageSize
  489. ) ;
  490. this.backBufferTile.show();
  491. }
  492. } else {
  493. // default effect is just to leave the existing tile
  494. // until the new one loads if this is a singleTile and
  495. // there was no change in resolution. Otherwise we
  496. // don't bother to show the backBufferTile at all
  497. if (this.layer.singleTile) {
  498. this.backBufferTile.show();
  499. } else {
  500. this.backBufferTile.hide();
  501. }
  502. }
  503. },
  504. /**
  505. * Method: show
  506. * Show the tile by showing its frame.
  507. */
  508. show: function() {
  509. this.frame.style.display = '';
  510. // Force a reflow on gecko based browsers to actually show the element
  511. // before continuing execution.
  512. if (OpenLayers.Util.indexOf(this.layer.SUPPORTED_TRANSITIONS,
  513. this.layer.transitionEffect) != -1) {
  514. if (OpenLayers.IS_GECKO === true) {
  515. this.frame.scrollLeft = this.frame.scrollLeft;
  516. }
  517. }
  518. },
  519. /**
  520. * Method: hide
  521. * Hide the tile by hiding its frame.
  522. */
  523. hide: function() {
  524. this.frame.style.display = 'none';
  525. },
  526. CLASS_NAME: "OpenLayers.Tile.Image"
  527. }
  528. );
  529. OpenLayers.Tile.Image.useBlankTile = (
  530. OpenLayers.BROWSER_NAME == "safari" ||
  531. OpenLayers.BROWSER_NAME == "opera");