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.
+
+
zoomify
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/zoomify.js b/examples/zoomify.js
new file mode 100644
index 0000000000..8da21f32ab
--- /dev/null
+++ b/examples/zoomify.js
@@ -0,0 +1,39 @@
+goog.require('ol.Map');
+goog.require('ol.RendererHint');
+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');
+
+var imgWidth = 8001;
+var imgHeight = 6943;
+var imgCenter = [imgWidth / 2, -imgHeight / 2];
+var url = 'http://mapy.mzk.cz/AA22/0103/';
+
+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]
+});
+
+var map = new ol.Map({
+ layers: [
+ new ol.layer.Tile({
+ source: source
+ })
+ ],
+ renderer: ol.RendererHint.CANVAS,
+ target: 'map',
+ view: new ol.View2D({
+ projection: proj,
+ center: imgCenter,
+ zoom: 0
+ })
+});
diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc
index 8f9a3053fc..ecb47640cb 100644
--- a/src/objectliterals.jsdoc
+++ b/src/objectliterals.jsdoc
@@ -840,6 +840,17 @@
* @todo stability experimental
*/
+/**
+ * @typedef {Object} ol.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.IconOptions
* @property {string|ol.expr.Expression} url Icon image URL.
@@ -954,3 +965,9 @@
* @property {number} maxZoom Maximum zoom.
* @todo stability experimental
*/
+
+/**
+ * @typedef {Object} ol.tilegrid.ZoomifyOptions
+ * @property {!Array.} resolutions Resolutions.
+ * @todo stability experimental
+ */
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/zoomifysource.exports b/src/ol/source/zoomifysource.exports
new file mode 100644
index 0000000000..57fce6e8b7
--- /dev/null
+++ b/src/ol/source/zoomifysource.exports
@@ -0,0 +1 @@
+@exportClass ol.source.Zoomify ol.source.ZoomifyOptions
diff --git a/src/ol/source/zoomifysource.js b/src/ol/source/zoomifysource.js
new file mode 100644
index 0000000000..fa0250e4e1
--- /dev/null
+++ b/src/ol/source/zoomifysource.js
@@ -0,0 +1,142 @@
+goog.provide('ol.source.Zoomify');
+
+goog.require('goog.array');
+goog.require('ol.TileCoord');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.proj');
+goog.require('ol.source.TileImage');
+goog.require('ol.tilegrid.Zoomify');
+
+
+
+/**
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {ol.source.ZoomifyOptions} options Zoomify options.
+ * @todo stability experimental
+ */
+ol.source.Zoomify = function(options) {
+
+ /**
+ * Prefix of URL template.
+ * @private
+ * @type {!string}
+ */
+ this.url_ = options.url;
+
+ /**
+ * Size of the image.
+ * @private
+ * @type {ol.Size}
+ */
+ this.size_ = options.size;
+
+ /**
+ * Depth of the Zoomify pyramid, number of tiers (zoom levels).
+ * @private
+ * @type {number}
+ */
+ this.numberOfTiers_ = 0;
+
+ /**
+ * Number of tiles up to the given tier of pyramid.
+ * @private
+ * @type {Array.}
+ */
+ this.tileCountUpToTier_ = null;
+
+ /**
+ * Size (in tiles) for each tier of pyramid.
+ * @private
+ * @type {Array.}
+ */
+ this.tierSizeInTiles_ = null;
+
+ /**
+ * Image size in pixels for each pyramid tier.
+ * @private
+ * @type {Array.}
+ */
+ this.tierImageSize_ = null;
+
+ var imageSize = [].concat(this.size_);
+ var tiles = [
+ Math.ceil(this.size_[0] / ol.DEFAULT_TILE_SIZE),
+ Math.ceil(this.size_[1] / ol.DEFAULT_TILE_SIZE)
+ ];
+ this.tierSizeInTiles_ = [tiles];
+ this.tierImageSize_ = [imageSize];
+
+ while (imageSize[0] > ol.DEFAULT_TILE_SIZE ||
+ imageSize[1] > ol.DEFAULT_TILE_SIZE) {
+
+ imageSize = [
+ Math.floor(imageSize[0] / 2),
+ Math.floor(imageSize[1] / 2)
+ ];
+ tiles = [
+ Math.ceil(imageSize[0] / ol.DEFAULT_TILE_SIZE),
+ Math.ceil(imageSize[1] / ol.DEFAULT_TILE_SIZE)
+ ];
+ this.tierSizeInTiles_.push(tiles);
+ this.tierImageSize_.push(imageSize);
+ }
+
+ this.tierSizeInTiles_.reverse();
+ this.tierImageSize_.reverse();
+ this.numberOfTiers_ = this.tierSizeInTiles_.length;
+ var resolutions = [1];
+ this.tileCountUpToTier_ = [0];
+ for (var i = 1; i < this.numberOfTiers_; i++) {
+ resolutions.unshift(Math.pow(2, i));
+ this.tileCountUpToTier_.push(
+ this.tierSizeInTiles_[i - 1][0] * this.tierSizeInTiles_[i - 1][1] +
+ this.tileCountUpToTier_[i - 1]
+ );
+ }
+
+
+ var createFromUrl = function(url) {
+ var template = url + '{tileIndex}/{z}-{x}-{y}.jpg';
+ return (
+ /**
+ * @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 * this.tierSizeInTiles_[tileCoord.z][0]) +
+ this.tileCountUpToTier_[tileCoord.z];
+ return template.replace('{tileIndex}', 'TileGroup' +
+ Math.floor((tileIndex) / ol.DEFAULT_TILE_SIZE))
+ .replace('{z}', '' + tileCoord.z)
+ .replace('{x}', '' + tileCoord.x)
+ .replace('{y}', '' + tileCoord.y);
+ }
+ }
+ );
+ };
+
+ var tileGrid = new ol.tilegrid.Zoomify({
+ resolutions: resolutions
+ });
+ var tileUrlFunction = ol.TileUrlFunction.withTileCoordTransform(
+ tileGrid.createTileCoordTransform(),
+ createFromUrl(this.url_));
+
+
+ goog.base(this, {
+ attributions: options.attributions,
+ crossOrigin: options.crossOrigin,
+ logo: options.logo,
+ tileGrid: tileGrid,
+ tileUrlFunction: tileUrlFunction
+ });
+
+};
+goog.inherits(ol.source.Zoomify, ol.source.TileImage);
diff --git a/src/ol/tilegrid/zoomifytilegrid.exports b/src/ol/tilegrid/zoomifytilegrid.exports
new file mode 100644
index 0000000000..132b391d5b
--- /dev/null
+++ b/src/ol/tilegrid/zoomifytilegrid.exports
@@ -0,0 +1 @@
+@exportClass ol.tilegrid.Zoomify ol.tilegrid.ZoomifyOptions
diff --git a/src/ol/tilegrid/zoomifytilegrid.js b/src/ol/tilegrid/zoomifytilegrid.js
new file mode 100644
index 0000000000..a88f54f861
--- /dev/null
+++ b/src/ol/tilegrid/zoomifytilegrid.js
@@ -0,0 +1,85 @@
+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 {ol.tilegrid.ZoomifyOptions} options Zoomify options.
+ * @todo stability experimental
+ */
+ol.tilegrid.Zoomify = function(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;
+ 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);
+ }
+ });
+};