From 3b7c795ecf93780c2774395a79191b0076ae2c43 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 19 Oct 2016 16:39:57 -0400 Subject: [PATCH] Add pixel coordinate utility function. Add a utility function to make it easier to create maps and tile layers in pixel coordinates with common settings and functions. It seems wasteful to have this code in each non-map example. Fix the deepzoom example. The IIP Server sends partial tiles at the right and bottom edge of the tile set. Our canvas renderer happens to work with this because it doesn't scale the tile images (which means it won't work on a retina tile). Added a capabilities flag so we can ask for that feature rather than specifying a renderer. --- examples/deepzoom/main.js | 43 +++++++++++++++-------- examples/tiles/main.js | 56 ++++------------------------- src/canvas/quadFeature.js | 1 + src/d3/quadFeature.js | 1 + src/gl/quadFeature.js | 1 + src/osmLayer.js | 2 ++ src/quadFeature.js | 2 ++ src/util/init.js | 74 +++++++++++++++++++++++++++++++++++++++ tests/cases/osmLayer.js | 22 ++++++++++++ 9 files changed, 138 insertions(+), 64 deletions(-) diff --git a/examples/deepzoom/main.js b/examples/deepzoom/main.js index 3ff947f9f6..743b337b75 100644 --- a/examples/deepzoom/main.js +++ b/examples/deepzoom/main.js @@ -1,7 +1,11 @@ +/* globals utils */ + // Run after the DOM loads $(function () { 'use strict'; + var query = utils.getQuery(); + // custom tileLayer for deepzoom var DeepZoom = function (arg) { if (!(this instanceof DeepZoom)) { @@ -13,38 +17,47 @@ $(function () { return geo.imageTile({ index: index, size: {x: this._options.tileWidth, y: this._options.tileHeight}, + queue: this._queue, url: this._options.url(source || index) }); }; }; - DeepZoom.defaults = $.extend({}, geo.tileLayer.defaults, { - minLevel: 0, - maxLevel: 9, + // We know the size of the image we are requesting, and we want to use + // pixel coordinates on our map. + var sizeX = 103583, sizeY = 70014, tileSize = 256; + var defaultParams = geo.util.pixelCoordinateParams( + '#map', sizeX, sizeY, tileSize, tileSize); + + DeepZoom.defaults = $.extend({}, geo.tileLayer.defaults, defaultParams.layer, { levelOffset: 8, - attribution: '', - wrapX: false, - wrapY: false, url: function (index) { return 'http://node15.cci.emory.edu/cgi-bin/iipsrv.fcgi?DeepZoom=/bigdata2/' + 'PYRAMIDS/CDSA/ACC_Diagnostic/nationwidechildrens.org_ACC.diagnostic_images.' + 'Level_1.304.4.0/TCGA-OR-A5J1-01Z-00-DX1.600C7D8C-F04C-4125-AF14-B1E76DC01A1E.' + 'svs.dzi.tif_files/' + (index.level + 8) + '/' + index.x + '_' + index.y + '.jpg'; - }, - tileOffset : function (level) { - var s = Math.pow(2, level - 1) * 256; - return {x: s, y: s}; } }); geo.inherit(DeepZoom, geo.tileLayer); geo.registerLayer('tiledFish', DeepZoom); // Create a map object - var map = geo.map({ - node: '#map' - }); + var map = geo.map($.extend({}, defaultParams.map, { + node: '#map', + clampBoundsX: false, + clampBoundsY: false, + clampZoom: false, + zoom: 2 + })); - // Add the osm layer with a custom tile url + // Add the osm layer with a custom tile url. + // We ask to use the quad.imageFixedScale feature, since the IIP server + // returns partial tiles at the right and bottom edges. If the tile server + // returns complete tiles that we need to crop, we would ask for the + // quad.imageCrop feature instead. map.createLayer( - 'tiledFish' + 'tiledFish', { + renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, + features: query.renderer ? undefined : ['quad.imageFixedScale'] + } ); }); diff --git a/examples/tiles/main.js b/examples/tiles/main.js index dd452e31e6..cb1cb11c69 100644 --- a/examples/tiles/main.js +++ b/examples/tiles/main.js @@ -100,8 +100,7 @@ $(function () { x: -98.0, y: 39.5 }, - maxBounds: {}, - zoom: query.zoom !== undefined ? parseFloat(query.zoom) : 3 + maxBounds: {} }; // Set the tile layer defaults to use the specified renderer and opacity var layerParams = { @@ -134,44 +133,14 @@ $(function () { } // For image tile servers, where we know the maximum width and height, use // a pixel coordinate system. - var w, h; if (query.w && query.h) { - w = parseInt(query.w, 10); - h = parseInt(query.h, 10); - // Set a pixel coordinate system where 0, 0 is the upper left and w, h is - // the lower-right. - /* If both ingcs and gcs are set to an empty string '', the coordinates - * will stay at pixel values, but the y values will from from [0, -h). If - * '+proj=longlat +axis=esu', '+proj=longlat +axis=enu' are used instead, - * the y coordinate will be reversed. It would be better to install a new - * 'inverse-y' projection into proj4, but this works without that change. - * The 'longlat' projection functionally is a no-op in this case. */ - mapParams.ingcs = '+proj=longlat +axis=esu'; - mapParams.gcs = '+proj=longlat +axis=enu'; - /* mapParams.ingcs = mapParams.gcs = ''; */ - mapParams.maxBounds = {left: 0, top: 0, right: w, bottom: h}; - mapParams.center = {x: w / 2, y: h / 2}; - mapParams.max = Math.ceil(Math.log(Math.max(w, h) / 256) / Math.log(2)); - mapParams.clampBoundsX = mapParams.clampBoundsY = true; - layerParams.maxLevel = mapParams.max; - layerParams.wrapX = layerParams.wrapY = false; - layerParams.tileOffset = function () { - return {x: 0, y: 0}; - }; - layerParams.attribution = ''; - layerParams.tileRounding = Math.ceil; - layerParams.tilesAtZoom = function (level) { - var scale = Math.pow(2, layerParams.maxLevel - level); - return { - x: Math.ceil(w / (layerParams.tileWidth || 256) / scale), - y: Math.ceil(h / (layerParams.tileHeight || 256) / scale) - }; - }; - layerParams.tilesMaxBounds = function (level) { - var scale = Math.pow(2, layerParams.maxLevel - level); - return {x: Math.floor(w / scale), y: Math.floor(h / scale)}; - }; + var pixelParams = geo.util.pixelCoordinateParams( + '#map', parseInt(query.w, 10), parseInt(query.h, 10), + layerParams.tileWidth || 256, layerParams.tileHeight || 256); + $.extend(mapParams, pixelParams.map); + $.extend(layerParams, pixelParams.layer); } + mapParams.zoom = query.zoom !== undefined ? parseFloat(query.zoom) : 3; // Parse additional query options if (query.x !== undefined) { mapParams.center.x = parseFloat(query.x); @@ -209,12 +178,6 @@ $(function () { if (query.tileHeight) { layerParams.tileHeight = parseInt(query.tileHeight, 10); } - if (w && h) { - mapParams.max = Math.ceil(Math.log(Math.max( - w / (layerParams.tileWidth || 256), - h / (layerParams.tileHeight || 256))) / Math.log(2)); - layerParams.maxLevel = mapParams.max; - } if (query.max !== undefined) { mapParams.max = parseFloat(query.max); } @@ -228,11 +191,6 @@ $(function () { if (!layerParams.maxLevel) { layerParams.maxLevel = 25; } - if (w && h) { - // unitsPerPixel is at zoom level 0. We want each pixel to be 1 at the - // maximum zoom - mapParams.unitsPerPixel = Math.pow(2, mapParams.max); - } if (query.unitsPerPixel !== undefined) { mapParams.unitsPerPixel = parseFloat(query.unitsPerPixel); } diff --git a/src/canvas/quadFeature.js b/src/canvas/quadFeature.js index 5c56677135..6750d18c6e 100644 --- a/src/canvas/quadFeature.js +++ b/src/canvas/quadFeature.js @@ -152,6 +152,7 @@ var capabilities = {}; capabilities[quadFeature.capabilities.color] = false; capabilities[quadFeature.capabilities.image] = true; capabilities[quadFeature.capabilities.imageCrop] = true; +capabilities[quadFeature.capabilities.imageFixedScale] = true; capabilities[quadFeature.capabilities.imageFull] = false; registerFeature('canvas', 'quad', canvas_quadFeature, capabilities); diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index 34610d1152..2f4c14d9a2 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -232,6 +232,7 @@ var capabilities = {}; capabilities[quadFeature.capabilities.color] = true; capabilities[quadFeature.capabilities.image] = true; capabilities[quadFeature.capabilities.imageCrop] = false; +capabilities[quadFeature.capabilities.imageFixedScale] = false; capabilities[quadFeature.capabilities.imageFull] = false; registerFeature('d3', 'quad', d3_quadFeature, capabilities); diff --git a/src/gl/quadFeature.js b/src/gl/quadFeature.js index 5d1279a1f7..c07e20ff1b 100644 --- a/src/gl/quadFeature.js +++ b/src/gl/quadFeature.js @@ -421,6 +421,7 @@ var capabilities = {}; capabilities[quadFeature.capabilities.color] = true; capabilities[quadFeature.capabilities.image] = true; capabilities[quadFeature.capabilities.imageCrop] = true; +capabilities[quadFeature.capabilities.imageFixedScale] = false; capabilities[quadFeature.capabilities.imageFull] = true; registerFeature('vgl', 'quad', gl_quadFeature, capabilities); diff --git a/src/osmLayer.js b/src/osmLayer.js index d146986cfa..f126f9a74d 100644 --- a/src/osmLayer.js +++ b/src/osmLayer.js @@ -54,6 +54,8 @@ module.exports = (function () { index: index, size: {x: this._options.tileWidth, y: this._options.tileHeight}, queue: this._queue, + overlap: this._options.tileOverlap, + scale: this._options.tileScale, url: this._options.url(urlParams.x, urlParams.y, urlParams.level || 0, this._options.subdomains) }); diff --git a/src/quadFeature.js b/src/quadFeature.js index b50c4600ba..1b7db080fe 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -479,6 +479,8 @@ quadFeature.capabilities = { image: 'quad.image', /* support for cropping quad images */ imageCrop: 'quad.imageCrop', + /* support for fixed-scale quad images */ + imageFixedScale: 'quad.imageFixedScale', /* support for arbitrary quad images */ imageFull: 'quad.imageFull' }; diff --git a/src/util/init.js b/src/util/init.js index 32150e03cf..23a4c2d7ff 100644 --- a/src/util/init.js +++ b/src/util/init.js @@ -449,6 +449,80 @@ } }, + /** + * Return recommended defaults for map parameters and osm or tile layer + * paramaters where the expected intent is to use the map in pixel + * coordinates (upper left is (0, 0), lower right is (width, height). The + * returned objects can be modified or extended. For instance, + * var results = pixelCoordinateParams('#map', 10000, 9000); + * geo.map($.extend(results.map, {clampZoom: false})); + * + * @param {string} [node] DOM selector for the map container + * @param {number} width width of the whole map contents in pixels + * @param {number} height height of the whole map contents in pixels + * @param {number} tileWidth if an osm or tile layer is going to be used, + * the width of a tile. + * @param {number} tileHeight if an osm or tile layer is going to be used, + * the height of a tile. + */ + pixelCoordinateParams: function (node, width, height, tileWidth, tileHeight) { + var mapW, mapH, tiled; + if (node) { + node = $(node); + mapW = node.innerWidth(); + mapH = node.innerHeight(); + } + tileWidth = tileWidth || width; + tileHeight = tileHeight || height; + tiled = (tileWidth !== width || tileHeight !== height); + var minLevel = Math.min(0, Math.floor(Math.log(Math.min( + (mapW || tileWidth) / tileWidth, + (mapH || tileHeight) / tileHeight)) / Math.log(2))), + maxLevel = Math.ceil(Math.log(Math.max( + width / tileWidth, + height / tileHeight)) / Math.log(2)); + var mapParams = { + node: node, + ingcs: '+proj=longlat +axis=esu', + gcs: '+proj=longlat +axis=enu', + maxBounds: {left: 0, top: 0, right: width, bottom: height}, + center: {x: width / 2, y: height / 2}, + min: minLevel, + max: maxLevel, + zoom: minLevel, + clampBoundsX: true, + clampBoundsY: true, + clampZoom: true + }; + var layerParams = { + maxLevel: maxLevel, + wrapX: false, + wrapY: false, + tileOffset: function () { + return {x: 0, y: 0}; + }, + attribution: '', + tileWidth: tileWidth, + tileHeight: tileHeight, + tileRounding: Math.ceil, + tilesAtZoom: tiled ? function (level) { + var scale = Math.pow(2, maxLevel - level); + return { + x: Math.ceil(width / tileWidth / scale), + y: Math.ceil(height / tileHeight / scale) + }; + } : undefined, + tilesMaxBounds: tiled ? function (level) { + var scale = Math.pow(2, maxLevel - level); + return { + x: Math.floor(width / scale), + y: Math.floor(height / scale) + }; + } : undefined + }; + return {map: mapParams, layer: layerParams}; + }, + /** * Report on one or all of the tracked timings. * diff --git a/tests/cases/osmLayer.js b/tests/cases/osmLayer.js index d527c61724..e616a9d684 100644 --- a/tests/cases/osmLayer.js +++ b/tests/cases/osmLayer.js @@ -280,6 +280,28 @@ describe('geo.core.osmLayer', function () { }); }); + describe('pixel coordinates', function () { + it('util.pixelCoordinateParams', function () { + var sizeX = 12345, sizeY = 5678, tileSize = 240; + var params = geo.util.pixelCoordinateParams('#map-osm-layer', sizeX, sizeY, tileSize, tileSize); + expect(params.map.ingcs).toBe('+proj=longlat +axis=esu'); + expect(params.layer.tileRounding).toBe(Math.ceil); + expect(params.layer.tileOffset()).toEqual({x: 0, y: 0}); + expect(params.layer.tilesAtZoom(3)).toEqual({x: 7, y: 3}); + expect(params.layer.tilesMaxBounds(3)).toEqual({x: 1543, y: 709}); + map = create_map(params.map); + map.createLayer('osm', $.extend( + {}, params.layer, {renderer: null, url: '/data/white.jpg', zoom: 3})); + expect(map.node().find('[data-tile-layer="0"]').length).toBe(1); + }); + waitForIt('.geo-tile-container', function () { + return map.node().find('.geo-tile-container').length > 0; + }); + it('check for tiles', function () { + expect(map.node().find('.geo-tile-container').length).toBeGreaterThan(0); + }); + it('destroy', destroy_map); + }); describe('geo.d3.osmLayer', function () { var layer, mapinfo = {}; it('test that tiles are created', function () {