diff --git a/examples/zoomify.html b/examples/zoomify.html new file mode 100644 index 0000000000..396a736392 --- /dev/null +++ b/examples/zoomify.html @@ -0,0 +1,50 @@ + + + + + + + + + + + Zoomify example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Zoomify example

+

Example of a Zoomify source.

+
+

See the zoomify.js source to see how this is done.

+
+
zoomify
+
+ +
+ +
+ + + + + + diff --git a/examples/zoomify.js b/examples/zoomify.js new file mode 100644 index 0000000000..4c0e3881ee --- /dev/null +++ b/examples/zoomify.js @@ -0,0 +1,52 @@ +goog.require('ol.Map'); +goog.require('ol.RendererHints'); +goog.require('ol.View2D'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.proj.Projection'); +goog.require('ol.proj.Units'); +goog.require('ol.source.Zoomify'); + +// This server does not support CORS, and so is incompatible with WebGL. +//var imgWidth = 8001; +//var imgHeight = 6943; +//var url = 'http://mapy.mzk.cz/AA22/0103/'; +//var crossOrigin = undefined; + +var imgWidth = 9911; +var imgHeight = 6100; +var url = 'http://vips.vtech.fr/cgi-bin/iipsrv.fcgi?zoomify=' + + '/mnt/MD1/AD00/plan_CHU-4HD-01/FOND.TIF/'; +var crossOrigin = 'anonymous'; + +var imgCenter = [imgWidth / 2, - imgHeight / 2]; + +// Maps always need a projection, but Zoomify layers are not geo-referenced, and +// are only measured in pixels. So, we create a fake projection that the map +// can use to properly display the layer. +var proj = new ol.proj.Projection({ + code: 'ZOOMIFY', + units: ol.proj.Units.PIXELS, + extent: [0, 0, imgWidth, imgHeight] +}); + +var source = new ol.source.Zoomify({ + url: url, + size: [imgWidth, imgHeight], + crossOrigin: crossOrigin +}); + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: source + }) + ], + renderers: ol.RendererHints.createFromQueryData(), + target: 'map', + view: new ol.View2D({ + projection: proj, + center: imgCenter, + zoom: 0 + }) +}); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 3f5b0319f8..bc5d5bf4c4 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -758,6 +758,17 @@ * @todo stability experimental */ +/** + * @typedef {Object} olx.source.ZoomifyOptions + * @property {Array.|undefined} attributions Attributions. + * @property {null|string|undefined} crossOrigin Cross origin setting for image + * requests. + * @property {string|undefined} logo Logo. + * @property {!string} url Prefix of URL template. + * @property {ol.Size} size Size of the image. + * @todo stability experimental + */ + /** * @typedef {Object} olx.style.CircleOptions * @property {ol.style.Fill|undefined} fill Fill style. @@ -841,3 +852,9 @@ * @property {number} maxZoom Maximum zoom. * @todo stability experimental */ + +/** + * @typedef {Object} olx.tilegrid.ZoomifyOptions + * @property {!Array.} resolutions Resolutions. + * @todo stability experimental + */ diff --git a/src/ol/map.js b/src/ol/map.js index 02c53fa5d5..83f1c9bb66 100644 --- a/src/ol/map.js +++ b/src/ol/map.js @@ -1203,7 +1203,7 @@ ol.Map.prototype.updateSize = function() { if (goog.isNull(targetElement)) { this.setSize(undefined); } else { - var size = goog.style.getSize(targetElement); + var size = goog.style.getContentBoxSize(targetElement); this.setSize([size.width, size.height]); } }; diff --git a/src/ol/observable.exports b/src/ol/observable.exports index 1d6e1ffcdf..1721abd5bd 100644 --- a/src/ol/observable.exports +++ b/src/ol/observable.exports @@ -1,3 +1,4 @@ +@exportSymbol ol.Observable @exportProperty ol.Observable.prototype.on @exportProperty ol.Observable.prototype.once @exportProperty ol.Observable.prototype.un diff --git a/src/ol/proj.exports b/src/ol/proj.exports index 79616aca0a..c415264d69 100644 --- a/src/ol/proj.exports +++ b/src/ol/proj.exports @@ -7,6 +7,7 @@ @exportProperty ol.proj.Units.DEGREES @exportProperty ol.proj.Units.FEET @exportProperty ol.proj.Units.METERS +@exportProperty ol.proj.Units.PIXELS @exportSymbol ol.proj.addProjection @exportSymbol ol.proj.get diff --git a/src/ol/proj/proj.js b/src/ol/proj/proj.js index bcb3f31e2d..c0c750a88f 100644 --- a/src/ol/proj/proj.js +++ b/src/ol/proj/proj.js @@ -40,7 +40,8 @@ ol.proj.ProjectionLike; ol.proj.Units = { DEGREES: 'degrees', FEET: 'ft', - METERS: 'm' + METERS: 'm', + PIXELS: 'pixels' }; diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index dd9da53a21..bf7d0982db 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -22,6 +22,9 @@ goog.require('ol.tilegrid.TileGrid'); * logo: (string|undefined), * opaque: (boolean|undefined), * projection: ol.proj.ProjectionLike, + * tileClass: (function(new: ol.ImageTile, ol.TileCoord, + * ol.TileState, string, ?string, + * ol.TileLoadFunctionType)|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileUrlFunction: (ol.TileUrlFunctionType|undefined)}} @@ -57,25 +60,33 @@ ol.source.TileImage = function(options) { ol.TileUrlFunction.nullTileUrlFunction; /** - * @private + * @protected * @type {?string} */ - this.crossOrigin_ = + this.crossOrigin = goog.isDef(options.crossOrigin) ? options.crossOrigin : null; /** - * @private + * @protected * @type {ol.TileCache} */ - this.tileCache_ = new ol.TileCache(); + this.tileCache = new ol.TileCache(); /** - * @private + * @protected * @type {ol.TileLoadFunctionType} */ - this.tileLoadFunction_ = goog.isDef(options.tileLoadFunction) ? + this.tileLoadFunction = goog.isDef(options.tileLoadFunction) ? options.tileLoadFunction : ol.source.TileImage.defaultTileLoadFunction; + /** + * @protected + * @type {function(new: ol.ImageTile, ol.TileCoord, ol.TileState, string, + * ?string, ol.TileLoadFunctionType)} + */ + this.tileClass = goog.isDef(options.tileClass) ? + options.tileClass : ol.ImageTile; + }; goog.inherits(ol.source.TileImage, ol.source.Tile); @@ -93,7 +104,7 @@ ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) { * @inheritDoc */ ol.source.TileImage.prototype.canExpireCache = function() { - return this.tileCache_.canExpireCache(); + return this.tileCache.canExpireCache(); }; @@ -101,7 +112,7 @@ ol.source.TileImage.prototype.canExpireCache = function() { * @inheritDoc */ ol.source.TileImage.prototype.expireCache = function(usedTiles) { - this.tileCache_.expireCache(usedTiles); + this.tileCache.expireCache(usedTiles); }; @@ -110,19 +121,19 @@ ol.source.TileImage.prototype.expireCache = function(usedTiles) { */ ol.source.TileImage.prototype.getTile = function(z, x, y, projection) { var tileCoordKey = this.getKeyZXY(z, x, y); - if (this.tileCache_.containsKey(tileCoordKey)) { - return /** @type {!ol.Tile} */ (this.tileCache_.get(tileCoordKey)); + if (this.tileCache.containsKey(tileCoordKey)) { + return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); } else { goog.asserts.assert(projection); var tileCoord = new ol.TileCoord(z, x, y); var tileUrl = this.tileUrlFunction(tileCoord, projection); - var tile = new ol.ImageTile( + var tile = new this.tileClass( tileCoord, goog.isDef(tileUrl) ? ol.TileState.IDLE : ol.TileState.EMPTY, goog.isDef(tileUrl) ? tileUrl : '', - this.crossOrigin_, - this.tileLoadFunction_); - this.tileCache_.set(tileCoordKey, tile); + this.crossOrigin, + this.tileLoadFunction); + this.tileCache.set(tileCoordKey, tile); return tile; } }; @@ -135,7 +146,7 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { // FIXME It should be possible to be more intelligent and avoid clearing the // FIXME cache. The tile URL function would need to be incorporated into the // FIXME cache key somehow. - this.tileCache_.clear(); + this.tileCache.clear(); this.tileUrlFunction = tileUrlFunction; this.dispatchChangeEvent(); }; @@ -146,7 +157,7 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { */ ol.source.TileImage.prototype.useTile = function(z, x, y) { var tileCoordKey = this.getKeyZXY(z, x, y); - if (this.tileCache_.containsKey(tileCoordKey)) { - this.tileCache_.get(tileCoordKey); + if (this.tileCache.containsKey(tileCoordKey)) { + this.tileCache.get(tileCoordKey); } }; diff --git a/src/ol/source/zoomifysource.exports b/src/ol/source/zoomifysource.exports new file mode 100644 index 0000000000..38f7c1657b --- /dev/null +++ b/src/ol/source/zoomifysource.exports @@ -0,0 +1 @@ +@exportSymbol ol.source.Zoomify diff --git a/src/ol/source/zoomifysource.js b/src/ol/source/zoomifysource.js new file mode 100644 index 0000000000..7e1a372d47 --- /dev/null +++ b/src/ol/source/zoomifysource.js @@ -0,0 +1,149 @@ +goog.provide('ol.source.Zoomify'); + +goog.require('goog.array'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('ol.ImageTile'); +goog.require('ol.TileCoord'); +goog.require('ol.TileState'); +goog.require('ol.TileUrlFunction'); +goog.require('ol.proj'); +goog.require('ol.source.TileImage'); +goog.require('ol.tilegrid.Zoomify'); + + + +/** + * @constructor + * @extends {ol.source.TileImage} + * @param {olx.source.ZoomifyOptions=} opt_options Options. + * @todo stability experimental + */ +ol.source.Zoomify = function(opt_options) { + + var options = goog.isDef(opt_options) ? opt_options : {}; + + var size = options.size; + var imageWidth = size[0]; + var imageHeight = size[1]; + + var tierSizeInTiles = []; + var tileSize = ol.DEFAULT_TILE_SIZE; + while (imageWidth > tileSize || imageHeight > tileSize) { + tierSizeInTiles.push([ + Math.ceil(imageWidth / tileSize), + Math.ceil(imageHeight / tileSize) + ]); + tileSize += tileSize; + } + tierSizeInTiles.push([1, 1]); + tierSizeInTiles.reverse(); + + var resolutions = [1]; + var tileCountUpToTier = [0]; + var i, ii; + for (i = 1, ii = tierSizeInTiles.length; i < ii; i++) { + resolutions.push(1 << i); + tileCountUpToTier.push( + tierSizeInTiles[i - 1][0] * tierSizeInTiles[i - 1][1] + + tileCountUpToTier[i - 1] + ); + } + resolutions.reverse(); + + var tileGrid = new ol.tilegrid.Zoomify({ + resolutions: resolutions + }); + + var url = options.url; + var tileUrlFunction = ol.TileUrlFunction.withTileCoordTransform( + tileGrid.createTileCoordTransform({extent: [0, 0, size[0], size[1]]}), + /** + * @this {ol.source.TileImage} + * @param {ol.TileCoord} tileCoord Tile Coordinate. + * @param {ol.proj.Projection} projection Projection. + * @return {string|undefined} Tile URL. + */ + function(tileCoord, projection) { + if (goog.isNull(tileCoord)) { + return undefined; + } else { + var tileIndex = tileCoord.x + + tileCoord.y * tierSizeInTiles[tileCoord.z][0] + + tileCountUpToTier[tileCoord.z]; + var tileGroup = (tileIndex / ol.DEFAULT_TILE_SIZE) | 0; + return url + 'TileGroup' + tileGroup + '/' + + tileCoord.z + '-' + tileCoord.x + '-' + tileCoord.y + '.jpg'; + } + }); + + goog.base(this, { + attributions: options.attributions, + crossOrigin: options.crossOrigin, + logo: options.logo, + tileClass: ol.source.ZoomifyTile_, + tileGrid: tileGrid, + tileUrlFunction: tileUrlFunction + }); + +}; +goog.inherits(ol.source.Zoomify, ol.source.TileImage); + + + +/** + * @constructor + * @extends {ol.ImageTile} + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.TileState} state State. + * @param {string} src Image source URI. + * @param {?string} crossOrigin Cross origin. + * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function. + * @private + */ +ol.source.ZoomifyTile_ = function( + tileCoord, state, src, crossOrigin, tileLoadFunction) { + + goog.base(this, tileCoord, state, src, crossOrigin, tileLoadFunction); + + /** + * @private + * @type {Object.} + */ + this.zoomifyImageByContext_ = {}; + +}; +goog.inherits(ol.source.ZoomifyTile_, ol.ImageTile); + + +/** + * @inheritDoc + */ +ol.source.ZoomifyTile_.prototype.getImage = function(opt_context) { + var key = goog.isDef(opt_context) ? goog.getUid(opt_context).toString() : ''; + if (key in this.zoomifyImageByContext_) { + return this.zoomifyImageByContext_[key]; + } else { + var image = goog.base(this, 'getImage', opt_context); + if (this.state == ol.TileState.LOADED) { + if (image.width == ol.DEFAULT_TILE_SIZE && + image.height == ol.DEFAULT_TILE_SIZE) { + this.zoomifyImageByContext_[key] = image; + return image; + } else { + var canvas = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + canvas.width = ol.DEFAULT_TILE_SIZE; + canvas.height = ol.DEFAULT_TILE_SIZE; + var context = /** @type {CanvasRenderingContext2D} */ + (canvas.getContext('2d')); + context.drawImage(image, 0, 0); + this.zoomifyImageByContext_[key] = canvas; + return canvas; + } + } else { + return image; + } + } +}; diff --git a/src/ol/tilegrid/zoomifytilegrid.exports b/src/ol/tilegrid/zoomifytilegrid.exports new file mode 100644 index 0000000000..afbfc424a2 --- /dev/null +++ b/src/ol/tilegrid/zoomifytilegrid.exports @@ -0,0 +1 @@ +@exportSymbol ol.tilegrid.Zoomify diff --git a/src/ol/tilegrid/zoomifytilegrid.js b/src/ol/tilegrid/zoomifytilegrid.js new file mode 100644 index 0000000000..115f1734c8 --- /dev/null +++ b/src/ol/tilegrid/zoomifytilegrid.js @@ -0,0 +1,86 @@ +goog.provide('ol.tilegrid.Zoomify'); + +goog.require('goog.math'); +goog.require('ol.TileCoord'); +goog.require('ol.proj'); +goog.require('ol.tilegrid.TileGrid'); + + + +/** + * @constructor + * @extends {ol.tilegrid.TileGrid} + * @param {olx.tilegrid.ZoomifyOptions=} opt_options Options. + * @todo stability experimental + */ +ol.tilegrid.Zoomify = function(opt_options) { + var options = goog.isDef(opt_options) ? opt_options : options; + goog.base(this, { + origin: [0, 0], + resolutions: options.resolutions + }); + +}; +goog.inherits(ol.tilegrid.Zoomify, ol.tilegrid.TileGrid); + + +/** + * @inheritDoc + */ +ol.tilegrid.Zoomify.prototype.createTileCoordTransform = function(opt_options) { + var options = goog.isDef(opt_options) ? opt_options : {}; + var minZ = this.minZoom; + var maxZ = this.maxZoom; + var tmpTileCoord = new ol.TileCoord(0, 0, 0); + /** @type {Array.} */ + var tileRangeByZ = null; + if (goog.isDef(options.extent)) { + tileRangeByZ = new Array(maxZ + 1); + var z; + for (z = 0; z <= maxZ; ++z) { + if (z < minZ) { + tileRangeByZ[z] = null; + } else { + tileRangeByZ[z] = this.getTileRangeForExtentAndZ(options.extent, z); + } + } + } + return ( + /** + * @param {ol.TileCoord} tileCoord Tile coordinate. + * @param {ol.proj.Projection} projection Projection. + * @param {ol.TileCoord=} opt_tileCoord Destination tile coordinate. + * @return {ol.TileCoord} Tile coordinate. + */ + function(tileCoord, projection, opt_tileCoord) { + var z = tileCoord.z; + if (z < minZ || maxZ < z) { + return null; + } + var n = Math.pow(2, z); + var x = tileCoord.x; + if (x < 0 || n <= x) { + return null; + } + var y = tileCoord.y; + if (y < -n || -1 < y) { + return null; + } + if (!goog.isNull(tileRangeByZ)) { + tmpTileCoord.z = z; + tmpTileCoord.x = x; + tmpTileCoord.y = -y - 1; + if (!tileRangeByZ[z].contains(tmpTileCoord)) { + return null; + } + } + if (goog.isDef(opt_tileCoord)) { + opt_tileCoord.z = z; + opt_tileCoord.x = x; + opt_tileCoord.y = -y - 1; + return opt_tileCoord; + } else { + return new ol.TileCoord(z, x, -y - 1); + } + }); +};