diff --git a/Apps/Sandcastle/gallery/Imagery Layers Palette.html b/Apps/Sandcastle/gallery/Imagery Layers Palette.html new file mode 100644 index 000000000000..658292cbfcbb --- /dev/null +++ b/Apps/Sandcastle/gallery/Imagery Layers Palette.html @@ -0,0 +1,205 @@ + + + + + + + + + Cesium Demo + + + + + + + + +
+

Loading...

+
+ + + + + diff --git a/Source/Renderer/UniformState.js b/Source/Renderer/UniformState.js index 759659750639..41da0ddb1265 100644 --- a/Source/Renderer/UniformState.js +++ b/Source/Renderer/UniformState.js @@ -150,6 +150,7 @@ define([ this._fogDensity = undefined; + this._imageryColorPalette = 1.0; this._imagerySplitPosition = 0.0; this._pixelSizePerMeter = undefined; this._geometricToleranceOverMeter = undefined; @@ -800,6 +801,16 @@ define([ get : function() { return this._imagerySplitPosition; } + }, + + /** + * @memberof UniformState.prototype + * @type {Array} + */ + imageryColorPalette : { + get : function() { + return this._imageryColorPalette; + } } }); @@ -962,6 +973,7 @@ define([ this._temeToPseudoFixed = Transforms.computeTemeToPseudoFixedMatrix(frameState.time, this._temeToPseudoFixed); this._imagerySplitPosition = frameState.imagerySplitPosition; + this._imageryColorPalette = frameState.imageryColorPalette; var fov = camera.frustum.fov; var viewport = this._viewport; var pixelSizePerMeter; diff --git a/Source/Scene/CustomTemplateImageryProvider.js b/Source/Scene/CustomTemplateImageryProvider.js new file mode 100644 index 000000000000..b9e5c9dfb24f --- /dev/null +++ b/Source/Scene/CustomTemplateImageryProvider.js @@ -0,0 +1,990 @@ +/*global define*/ +define([ + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Cartographic', + '../Core/combine', + '../Core/Credit', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/DeveloperError', + '../Core/Event', + '../Core/freezeObject', + '../Core/GeographicTilingScheme', + '../Core/isArray', + '../Core/loadJson', + '../Core/loadText', + '../Core/loadWithXhr', + '../Core/loadXML', + '../Core/Math', + '../Core/Rectangle', + '../Core/TileProviderError', + '../Core/WebMercatorTilingScheme', + '../ThirdParty/when', + './ImageryProvider' + ], function( + Cartesian2, + Cartesian3, + Cartographic, + combine, + Credit, + defaultValue, + defined, + defineProperties, + DeveloperError, + Event, + freezeObject, + GeographicTilingScheme, + isArray, + loadJson, + loadText, + loadWithXhr, + loadXML, + CesiumMath, + Rectangle, + TileProviderError, + WebMercatorTilingScheme, + when, + ImageryProvider + ) { + 'use strict'; + + /** + * Provides imagery by requesting tiles using a specified URL template. + * + * @alias CustomTemplateImageryProvider + * @constructor + * + * @param {Promise.|Object} [options] Object with the following properties: + * @param {String} options.url The URL template to use to request tiles. It has the following keywords: + * + * @param {String} [options.pickFeaturesUrl] The URL template to use to pick features. If this property is not specified, + * {@link CustomTemplateImageryProvider#pickFeatures} will immediately returned undefined, indicating no + * features picked. The URL template supports all of the keywords supported by the url + * parameter, plus the following: + * + * @param {Object} [options.urlSchemeZeroPadding] Gets the URL scheme zero padding for each tile coordinate. The format is '000' where + * each coordinate will be padded on the left with zeros to match the width of the passed string of zeros. e.g. Setting: + * urlSchemeZeroPadding : { '{x}' : '0000'} + * will cause an 'x' value of 12 to return the string '0012' for {x} in the generated URL. + * It the passed object has the following keywords: + * + * @param {String|String[]} [options.subdomains='abc'] The subdomains to use for the {s} placeholder in the URL template. + * If this parameter is a single string, each character in the string is a subdomain. If it is + * an array, each element in the array is a subdomain. + * @param {Object} [options.proxy] A proxy to use for requests. This object is expected to have a getURL function which returns the proxied URL. + * @param {Credit|String} [options.credit=''] A credit for the data source, which is displayed on the canvas. + * @param {Number} [options.minimumLevel=0] The minimum level-of-detail supported by the imagery provider. Take care when specifying + * this that the number of tiles at the minimum level is small, such as four or less. A larger number is likely + * to result in rendering problems. + * @param {Number} [options.maximumLevel] The maximum level-of-detail supported by the imagery provider, or undefined if there is no limit. + * @param {Rectangle} [options.rectangle=Rectangle.MAX_VALUE] The rectangle, in radians, covered by the image. + * @param {TilingScheme} [options.tilingScheme=WebMercatorTilingScheme] The tiling scheme specifying how the ellipsoidal + * surface is broken into tiles. If this parameter is not provided, a {@link WebMercatorTilingScheme} + * is used. + * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If the tilingScheme is specified, + * this parameter is ignored and the tiling scheme's ellipsoid is used instead. If neither + * parameter is specified, the WGS84 ellipsoid is used. + * @param {Number} [options.tileWidth=256] Pixel width of image tiles. + * @param {Number} [options.tileHeight=256] Pixel height of image tiles. + * @param {Boolean} [options.hasAlphaChannel=true] true if the images provided by this imagery provider + * include an alpha channel; otherwise, false. If this property is false, an alpha channel, if + * present, will be ignored. If this property is true, any images without an alpha channel will + * be treated as if their alpha is 1.0 everywhere. When this property is false, memory usage + * and texture upload time are potentially reduced. + * @param {GetFeatureInfoFormat[]} [options.getFeatureInfoFormats] The formats in which to get feature information at a + * specific location when {@link CustomTemplateImageryProvider#pickFeatures} is invoked. If this + * parameter is not specified, feature picking is disabled. + * @param {Boolean} [options.enablePickFeatures=true] If true, {@link CustomTemplateImageryProvider#pickFeatures} will + * request the options.pickFeaturesUrl and attempt to interpret the features included in the response. If false, + * {@link CustomTemplateImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable + * features) without communicating with the server. Set this property to false if you know your data + * source does not support picking features or if you don't want this provider's features to be pickable. Note + * that this can be dynamically overridden by modifying the {@link UriTemplateImageryProvider#enablePickFeatures} + * property. + * + * + * @example + * // Access Natural Earth II imagery, which uses a TMS tiling scheme and Geographic (EPSG:4326) project + * var tms = new Cesium.CustomTemplateImageryProvider({ + * url : 'https://cesiumjs.org/tilesets/imagery/naturalearthii/{z}/{x}/{reverseY}.jpg', + * credit : '© Analytical Graphics, Inc.', + * tilingScheme : new Cesium.GeographicTilingScheme(), + * maximumLevel : 5 + * }); + * // Access the CartoDB Positron basemap, which uses an OpenStreetMap-like tiling scheme. + * var positron = new Cesium.CustomTemplateImageryProvider({ + * url : 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', + * credit : 'Map tiles by CartoDB, under CC BY 3.0. Data by OpenStreetMap, under ODbL.' + * }); + * // Access a Web Map Service (WMS) server. + * var wms = new Cesium.CustomTemplateImageryProvider({ + * url : 'https://programs.communications.gov.au/geoserver/ows?tiled=true&' + + * 'transparent=true&format=image%2Fpng&exceptions=application%2Fvnd.ogc.se_xml&' + + * 'styles=&service=WMS&version=1.1.1&request=GetMap&' + + * 'layers=public%3AMyBroadband_Availability&srs=EPSG%3A3857&' + + * 'bbox={westProjected}%2C{southProjected}%2C{eastProjected}%2C{northProjected}&' + + * 'width=256&height=256', + * rectangle : Cesium.Rectangle.fromDegrees(96.799393, -43.598214999057824, 153.63925700000001, -9.2159219997013) + * }); + * + * @see ArcGisMapServerImageryProvider + * @see BingMapsImageryProvider + * @see GoogleEarthImageryProvider + * @see createOpenStreetMapImageryProvider + * @see SingleTileImageryProvider + * @see createTileMapServiceImageryProvider + * @see WebMapServiceImageryProvider + * @see WebMapTileServiceImageryProvider + */ + function CustomTemplateImageryProvider(options) { + //>>includeStart('debug', pragmas.debug); + if (!defined(options)) { + throw new DeveloperError('options is required.'); + } + if (!when.isPromise(options) && !defined(options.url)) { + throw new DeveloperError('options is required.'); + } + //>>includeEnd('debug'); + + this._errorEvent = new Event(); + + this._url = undefined; + this._urlSchemeZeroPadding = undefined; + this._pickFeaturesUrl = undefined; + this._proxy = undefined; + this._tileWidth = undefined; + this._tileHeight = undefined; + this._maximumLevel = undefined; + this._minimumLevel = undefined; + this._tilingScheme = undefined; + this._rectangle = undefined; + this._tileDiscardPolicy = undefined; + this._credit = undefined; + this._hasAlphaChannel = undefined; + this._readyPromise = undefined; + this._palette = undefined; + this._min = undefined; + this._max = undefined; + + /** + * Gets or sets a value indicating whether feature picking is enabled. If true, {@link CustomTemplateImageryProvider#pickFeatures} will + * request the options.pickFeaturesUrl and attempt to interpret the features included in the response. If false, + * {@link CustomTemplateImageryProvider#pickFeatures} will immediately return undefined (indicating no pickable + * features) without communicating with the server. Set this property to false if you know your data + * source does not support picking features or if you don't want this provider's features to be pickable. + * @type {Boolean} + * @default true + */ + this.enablePickFeatures = true; + + this.reinitialize(options); + } + + defineProperties(CustomTemplateImageryProvider.prototype, { + /** + * Gets the URL template to use to request tiles. It has the following keywords: + * + * @memberof CustomTemplateImageryProvider.prototype + * @type {String} + * @readonly + */ + url : { + get : function() { + return this._url; + } + }, + + /** + * Gets the URL scheme zero padding for each tile coordinate. The format is '000' where each coordinate will be padded on + * the left with zeros to match the width of the passed string of zeros. e.g. Setting: + * urlSchemeZeroPadding : { '{x}' : '0000'} + * will cause an 'x' value of 12 to return the string '0012' for {x} in the generated URL. + * It has the following keywords: + * + * @memberof CustomTemplateImageryProvider.prototype + * @type {Object} + * @readonly + */ + urlSchemeZeroPadding : { + get : function() { + return this._urlSchemeZeroPadding; + } + }, + + + /** + * Gets the URL template to use to use to pick features. If this property is not specified, + * {@link CustomTemplateImageryProvider#pickFeatures} will immediately returned undefined, indicating no + * features picked. The URL template supports all of the keywords supported by the + * {@link CustomTemplateImageryProvider#url} property, plus the following: + * + * @type {String} + * @readonly + */ + pickFeaturesUrl : { + get : function() { + return this._pickFeaturesUrl; + } + }, + + /** + * Gets the proxy used by this provider. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Proxy} + * @readonly + * @default undefined + */ + proxy : { + get : function() { + return this._proxy; + } + }, + + /** + * Gets the width of each tile, in pixels. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Number} + * @readonly + * @default 256 + */ + tileWidth : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('tileWidth must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._tileWidth; + } + }, + + /** + * Gets the height of each tile, in pixels. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Number} + * @readonly + * @default 256 + */ + tileHeight: { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('tileHeight must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._tileHeight; + } + }, + + /** + * Gets the maximum level-of-detail that can be requested, or undefined if there is no limit. + * This function should not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Number} + * @readonly + * @default undefined + */ + maximumLevel : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('maximumLevel must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._maximumLevel; + } + }, + + /** + * Gets the minimum level-of-detail that can be requested. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Number} + * @readonly + * @default 0 + */ + minimumLevel : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('minimumLevel must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._minimumLevel; + } + }, + + /** + * Gets the tiling scheme used by this provider. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {TilingScheme} + * @readonly + * @default new WebMercatorTilingScheme() + */ + tilingScheme : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('tilingScheme must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._tilingScheme; + } + }, + + /** + * Gets the rectangle, in radians, of the imagery provided by this instance. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Rectangle} + * @readonly + * @default tilingScheme.rectangle + */ + rectangle : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('rectangle must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._rectangle; + } + }, + + /** + * Gets the tile discard policy. If not undefined, the discard policy is responsible + * for filtering out "missing" tiles via its shouldDiscardImage function. If this function + * returns undefined, no tiles are filtered. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {TileDiscardPolicy} + * @readonly + * @default undefined + */ + tileDiscardPolicy : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('tileDiscardPolicy must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._tileDiscardPolicy; + } + }, + + /** + * Gets an event that is raised when the imagery provider encounters an asynchronous error. By subscribing + * to the event, you will be notified of the error and can potentially recover from it. Event listeners + * are passed an instance of {@link TileProviderError}. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Event} + * @readonly + */ + errorEvent : { + get : function() { + return this._errorEvent; + } + }, + + /** + * Gets a value indicating whether or not the provider is ready for use. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Boolean} + * @readonly + */ + ready : { + get : function() { + return defined(this._urlParts); + } + }, + + /** + * Gets a promise that resolves to true when the provider is ready for use. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Promise.} + * @readonly + */ + readyPromise : { + get : function() { + return this._readyPromise; + } + }, + + /** + * Gets the credit to display when this imagery provider is active. Typically this is used to credit + * the source of the imagery. This function should not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Credit} + * @readonly + * @default undefined + */ + credit : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('credit must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._credit; + } + }, + + /** + * Gets a value indicating whether or not the images provided by this imagery provider + * include an alpha channel. If this property is false, an alpha channel, if present, will + * be ignored. If this property is true, any images without an alpha channel will be treated + * as if their alpha is 1.0 everywhere. When this property is false, memory usage + * and texture upload time are reduced. This function should + * not be called before {@link ImageryProvider#ready} returns true. + * @memberof CustomTemplateImageryProvider.prototype + * @type {Boolean} + * @readonly + * @default true + */ + hasAlphaChannel : { + get : function() { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('hasAlphaChannel must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return this._hasAlphaChannel; + } + } + }); + + /** + * Reinitializes this instance. Reinitializing an instance already in use is supported, but it is not + * recommended because existing tiles provided by the imagery provider will not be updated. + * + * @param {Promise.|Object} options Any of the options that may be passed to the {@link CustomTemplateImageryProvider} constructor. + */ + CustomTemplateImageryProvider.prototype.reinitialize = function(options) { + var that = this; + that._readyPromise = when(options).then(function(properties) { + //>>includeStart('debug', pragmas.debug); + if (!defined(properties)) { + throw new DeveloperError('options is required.'); + } + if (!defined(properties.url)) { + throw new DeveloperError('options.url is required.'); + } + //>>includeEnd('debug'); + that.enablePickFeatures = defaultValue(properties.enablePickFeatures, that.enablePickFeatures); + that._url = properties.url; + that._urlSchemeZeroPadding = defaultValue(properties.urlSchemeZeroPadding, that.urlSchemeZeroPadding); + that._pickFeaturesUrl = properties.pickFeaturesUrl; + that._proxy = properties.proxy; + that._tileDiscardPolicy = properties.tileDiscardPolicy; + that._getFeatureInfoFormats = properties.getFeatureInfoFormats; + + that._subdomains = properties.subdomains; + if (isArray(that._subdomains)) { + that._subdomains = that._subdomains.slice(); + } else if (defined(that._subdomains) && that._subdomains.length > 0) { + that._subdomains = that._subdomains.split(''); + } else { + that._subdomains = ['a', 'b', 'c']; + } + + that._tileWidth = defaultValue(properties.tileWidth, 256); + that._tileHeight = defaultValue(properties.tileHeight, 256); + that._minimumLevel = defaultValue(properties.minimumLevel, 0); + that._maximumLevel = properties.maximumLevel; + that._tilingScheme = defaultValue(properties.tilingScheme, new WebMercatorTilingScheme({ ellipsoid : properties.ellipsoid })); + that._rectangle = defaultValue(properties.rectangle, that._tilingScheme.rectangle); + that._rectangle = Rectangle.intersection(that._rectangle, that._tilingScheme.rectangle); + that._hasAlphaChannel = defaultValue(properties.hasAlphaChannel, true); + + that._colorPalette = defaultValue(properties.colorPalette); + that._min = defaultValue(properties.min, 0); + that._max = defaultValue(properties.max, 100); + + var credit = properties.credit; + if (typeof credit === 'string') { + credit = new Credit(credit); + } + that._credit = credit; + + that._urlParts = urlTemplateToParts(that._url, tags); + that._pickFeaturesUrlParts = urlTemplateToParts(that._pickFeaturesUrl, pickFeaturesTags); + return true; + }); + }; + + /** + * Gets the credits to be displayed when a given tile is displayed. + * + * @param {Number} x The tile X coordinate. + * @param {Number} y The tile Y coordinate. + * @param {Number} level The tile level; + * @returns {Credit[]} The credits to be displayed when the tile is displayed. + * + * @exception {DeveloperError} getTileCredits must not be called before the imagery provider is ready. + */ + CustomTemplateImageryProvider.prototype.getTileCredits = function(x, y, level) { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('getTileCredits must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + return undefined; + }; + + /** + * Requests the image for a given tile. This function should + * not be called before {@link CustomTemplateImageryProvider#ready} returns true. + * + * @param {Number} x The tile X coordinate. + * @param {Number} y The tile Y coordinate. + * @param {Number} level The tile level. + * @returns {Promise.|undefined} A promise for the image that will resolve when the image is available, or + * undefined if there are too many active requests to the server, and the request + * should be retried later. The resolved image may be either an + * Image or a Canvas DOM object. + */ + CustomTemplateImageryProvider.prototype.requestImage = function(x, y, level) { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('requestImage must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + var url = buildImageUrl(this, x, y, level); + var imagePromise = ImageryProvider.loadImage(this, url); + if(!defined(imagePromise)) { + return imagePromise; + } + + return when(imagePromise, function(image) { + return image; + }); + }; + + /** + * Asynchronously determines what features, if any, are located at a given longitude and latitude within + * a tile. This function should not be called before {@link ImageryProvider#ready} returns true. + * + * @param {Number} x The tile X coordinate. + * @param {Number} y The tile Y coordinate. + * @param {Number} level The tile level. + * @param {Number} longitude The longitude at which to pick features. + * @param {Number} latitude The latitude at which to pick features. + * @return {Promise.|undefined} A promise for the picked features that will resolve when the asynchronous + * picking completes. The resolved value is an array of {@link ImageryLayerFeatureInfo} + * instances. The array may be empty if no features are found at the given location. + * It may also be undefined if picking is not supported. + */ + CustomTemplateImageryProvider.prototype.pickFeatures = function(x, y, level, longitude, latitude) { + //>>includeStart('debug', pragmas.debug); + if (!this.ready) { + throw new DeveloperError('pickFeatures must not be called before the imagery provider is ready.'); + } + //>>includeEnd('debug'); + + if (!this.enablePickFeatures) { + return undefined; + } + + var format = this._getFeatureInfoFormats[0]; + + return format.callback(x, y, level, longitude, latitude); + }; + + function buildImageUrl(imageryProvider, x, y, level) { + degreesScratchComputed = false; + projectedScratchComputed = false; + + return buildUrl(imageryProvider, imageryProvider._urlParts, function(partFunction) { + return partFunction(imageryProvider, x, y, level); + }); + } + + function buildUrl(imageryProvider, parts, partFunctionInvoker) { + var url = ''; + + for (var i = 0; i < parts.length; ++i) { + var part = parts[i]; + if (typeof part === 'string') { + url += part; + } else { + url += encodeURIComponent(partFunctionInvoker(part)); + } + } + + var proxy = imageryProvider._proxy; + if (defined(proxy)) { + url = proxy.getURL(url); + } + + return url; + } + + function urlTemplateToParts(url, tags) { + if (!defined(url)) { + return undefined; + } + + var parts = []; + var nextIndex = 0; + var minIndex; + var minTag; + var tagList = Object.keys(tags); + + while (nextIndex < url.length) { + minIndex = Number.MAX_VALUE; + minTag = undefined; + + for (var i = 0; i < tagList.length; ++i) { + var thisIndex = url.indexOf(tagList[i], nextIndex); + if (thisIndex >= 0 && thisIndex < minIndex) { + minIndex = thisIndex; + minTag = tagList[i]; + } + } + + if (!defined(minTag)) { + parts.push(url.substring(nextIndex)); + nextIndex = url.length; + } else { + if (nextIndex < minIndex) { + parts.push(url.substring(nextIndex, minIndex)); + } + parts.push(tags[minTag]); + nextIndex = minIndex + minTag.length; + } + } + + return parts; + } + + function padWithZerosIfNecessary(imageryProvider, key, value) { + if (imageryProvider && + imageryProvider.urlSchemeZeroPadding && + imageryProvider.urlSchemeZeroPadding.hasOwnProperty(key) ) + { + var paddingTemplate = imageryProvider.urlSchemeZeroPadding[key]; + if (typeof paddingTemplate === 'string') { + var paddingTemplateWidth = paddingTemplate.length; + if (paddingTemplateWidth > 1) { + value = (value.length >= paddingTemplateWidth) ? value : new Array(paddingTemplateWidth - value.toString().length + 1).join('0') + value; + } + } + } + return value; + } + + function xTag(imageryProvider, x, y, level) { + return padWithZerosIfNecessary(imageryProvider, '{x}', x); + } + + function reverseXTag(imageryProvider, x, y, level) { + var reverseX = imageryProvider.tilingScheme.getNumberOfXTilesAtLevel(level) - x - 1; + return padWithZerosIfNecessary(imageryProvider, '{reverseX}', reverseX); + } + + function yTag(imageryProvider, x, y, level) { + return padWithZerosIfNecessary(imageryProvider, '{y}', y); + } + + function reverseYTag(imageryProvider, x, y, level) { + var reverseY = imageryProvider.tilingScheme.getNumberOfYTilesAtLevel(level) - y - 1; + return padWithZerosIfNecessary(imageryProvider, '{reverseY}', reverseY); + } + + function reverseZTag(imageryProvider, x, y, level) { + var maximumLevel = imageryProvider.maximumLevel; + var reverseZ = defined(maximumLevel) && level < maximumLevel ? maximumLevel - level - 1 : level; + return padWithZerosIfNecessary(imageryProvider, '{reverseZ}', reverseZ); + } + + function zTag(imageryProvider, x, y, level) { + return padWithZerosIfNecessary(imageryProvider, '{z}', level); + } + + function sTag(imageryProvider, x, y, level) { + var index = (x + y + level) % imageryProvider._subdomains.length; + return imageryProvider._subdomains[index]; + } + + function minValTag(imageryProvider, x, y, level) { + return padWithZerosIfNecessary(imageryProvider, '{min}', imageryProvider._min); + } + + function maxValTag(imageryProvider, x, y, level) { + return padWithZerosIfNecessary(imageryProvider, '{max}', imageryProvider._max); + } + + var degreesScratchComputed = false; + var degreesScratch = new Rectangle(); + + function computeDegrees(imageryProvider, x, y, level) { + if (degreesScratchComputed) { + return; + } + + imageryProvider.tilingScheme.tileXYToRectangle(x, y, level, degreesScratch); + degreesScratch.west = CesiumMath.toDegrees(degreesScratch.west); + degreesScratch.south = CesiumMath.toDegrees(degreesScratch.south); + degreesScratch.east = CesiumMath.toDegrees(degreesScratch.east); + degreesScratch.north = CesiumMath.toDegrees(degreesScratch.north); + + degreesScratchComputed = true; + } + + function westDegreesTag(imageryProvider, x, y, level) { + computeDegrees(imageryProvider, x, y, level); + return degreesScratch.west; + } + + function southDegreesTag(imageryProvider, x, y, level) { + computeDegrees(imageryProvider, x, y, level); + return degreesScratch.south; + } + + function eastDegreesTag(imageryProvider, x, y, level) { + computeDegrees(imageryProvider, x, y, level); + return degreesScratch.east; + } + + function northDegreesTag(imageryProvider, x, y, level) { + computeDegrees(imageryProvider, x, y, level); + return degreesScratch.north; + } + + var projectedScratchComputed = false; + var projectedScratch = new Rectangle(); + + function computeProjected(imageryProvider, x, y, level) { + if (projectedScratchComputed) { + return; + } + + imageryProvider.tilingScheme.tileXYToNativeRectangle(x, y, level, projectedScratch); + + projectedScratchComputed = true; + } + + function westProjectedTag(imageryProvider, x, y, level) { + computeProjected(imageryProvider, x, y, level); + return projectedScratch.west; + } + + function southProjectedTag(imageryProvider, x, y, level) { + computeProjected(imageryProvider, x, y, level); + return projectedScratch.south; + } + + function eastProjectedTag(imageryProvider, x, y, level) { + computeProjected(imageryProvider, x, y, level); + return projectedScratch.east; + } + + function northProjectedTag(imageryProvider, x, y, level) { + computeProjected(imageryProvider, x, y, level); + return projectedScratch.north; + } + + function widthTag(imageryProvider, x, y, level) { + return imageryProvider.tileWidth; + } + + function heightTag(imageryProvider, x, y, level) { + return imageryProvider.tileHeight; + } + + var ijScratchComputed = false; + var ijScratch = new Cartesian2(); + + function iTag(imageryProvider, x, y, level, longitude, latitude, format) { + computeIJ(imageryProvider, x, y, level, longitude, latitude); + return ijScratch.x; + } + + function jTag(imageryProvider, x, y, level, longitude, latitude, format) { + computeIJ(imageryProvider, x, y, level, longitude, latitude); + return ijScratch.y; + } + + function reverseITag(imageryProvider, x, y, level, longitude, latitude, format) { + computeIJ(imageryProvider, x, y, level, longitude, latitude); + return imageryProvider.tileWidth - ijScratch.x - 1; + } + + function reverseJTag(imageryProvider, x, y, level, longitude, latitude, format) { + computeIJ(imageryProvider, x, y, level, longitude, latitude); + return imageryProvider.tileHeight - ijScratch.y - 1; + } + + var rectangleScratch = new Rectangle(); + + function computeIJ(imageryProvider, x, y, level, longitude, latitude, format) { + if (ijScratchComputed) { + return; + } + + computeLongitudeLatitudeProjected(imageryProvider, x, y, level, longitude, latitude); + var projected = longitudeLatitudeProjectedScratch; + + var rectangle = imageryProvider.tilingScheme.tileXYToNativeRectangle(x, y, level, rectangleScratch); + ijScratch.x = (imageryProvider.tileWidth * (projected.x - rectangle.west) / rectangle.width) | 0; + ijScratch.y = (imageryProvider.tileHeight * (rectangle.north - projected.y) / rectangle.height) | 0; + ijScratchComputed = true; + } + + function longitudeDegreesTag(imageryProvider, x, y, level, longitude, latitude, format) { + return CesiumMath.toDegrees(longitude); + } + + function latitudeDegreesTag(imageryProvider, x, y, level, longitude, latitude, format) { + return CesiumMath.toDegrees(latitude); + } + + var longitudeLatitudeProjectedScratchComputed = false; + var longitudeLatitudeProjectedScratch = new Cartesian3(); + + function longitudeProjectedTag(imageryProvider, x, y, level, longitude, latitude, format) { + computeLongitudeLatitudeProjected(imageryProvider, x, y, level, longitude, latitude); + return longitudeLatitudeProjectedScratch.x; + } + + function latitudeProjectedTag(imageryProvider, x, y, level, longitude, latitude, format) { + computeLongitudeLatitudeProjected(imageryProvider, x, y, level, longitude, latitude); + return longitudeLatitudeProjectedScratch.y; + } + + var cartographicScratch = new Cartographic(); + + function computeLongitudeLatitudeProjected(imageryProvider, x, y, level, longitude, latitude, format) { + if (longitudeLatitudeProjectedScratchComputed) { + return; + } + + var projected; + if (imageryProvider.tilingScheme instanceof GeographicTilingScheme) { + longitudeLatitudeProjectedScratch.x = CesiumMath.toDegrees(longitude); + longitudeLatitudeProjectedScratch.y = CesiumMath.toDegrees(latitude); + } else { + var cartographic = cartographicScratch; + cartographic.longitude = longitude; + cartographic.latitude = latitude; + projected = imageryProvider.tilingScheme.projection.project(cartographic, longitudeLatitudeProjectedScratch); + } + + longitudeLatitudeProjectedScratchComputed = true; + } + + function formatTag(imageryProvider, x, y, level, longitude, latitude, format) { + return format; + } + + var tags = { + '{x}': xTag, + '{y}': yTag, + '{z}': zTag, + '{s}': sTag, + '{reverseX}': reverseXTag, + '{reverseY}': reverseYTag, + '{reverseZ}': reverseZTag, + '{westDegrees}': westDegreesTag, + '{southDegrees}': southDegreesTag, + '{eastDegrees}': eastDegreesTag, + '{northDegrees}': northDegreesTag, + '{westProjected}': westProjectedTag, + '{southProjected}': southProjectedTag, + '{eastProjected}': eastProjectedTag, + '{northProjected}': northProjectedTag, + '{width}': widthTag, + '{height}': heightTag, + '{min}': minValTag, + '{max}': maxValTag + }; + + var pickFeaturesTags = combine(tags, { + '{i}' : iTag, + '{j}' : jTag, + '{reverseI}' : reverseITag, + '{reverseJ}' : reverseJTag, + '{longitudeDegrees}' : longitudeDegreesTag, + '{latitudeDegrees}' : latitudeDegreesTag, + '{longitudeProjected}' : longitudeProjectedTag, + '{latitudeProjected}' : latitudeProjectedTag, + '{format}' : formatTag + }); + + return CustomTemplateImageryProvider; +}); diff --git a/Source/Scene/FrameState.js b/Source/Scene/FrameState.js index f28094fd9c7b..9afae4f822a4 100644 --- a/Source/Scene/FrameState.js +++ b/Source/Scene/FrameState.js @@ -242,6 +242,12 @@ define([ * @default [] */ this.frustumSplits = []; + + /* The color palette to use when rendering imagery layers. + * @type {Array} + * @default undefined + */ + this.imageryColorPalette = 1.0; } /** diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 7ed5d9a3496d..83dfd327aeea 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -62,7 +62,7 @@ define([ return useWebMercatorProjection ? get2DYPositionFractionMercatorProjection : get2DYPositionFractionGeographicProjection; } - GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog) { + GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, numberOfPaletteTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, colorPaletteKeys, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog) { var quantization = 0; var quantizationDefine = ''; @@ -88,7 +88,8 @@ define([ (useWebMercatorProjection << 12) | (enableFog << 13) | (quantization << 14) | - (applySplit << 15); + (applySplit << 15) | + (colorPaletteKeys << 16); var surfaceShader = surfaceTile.surfaceShader; if (defined(surfaceShader) && @@ -112,6 +113,7 @@ define([ vs.defines.push(quantizationDefine); fs.defines.push('TEXTURE_UNITS ' + numberOfDayTextures); + fs.defines.push('PALETTE_UNITS ' + numberOfPaletteTextures); if (applyBrightness) { fs.defines.push('APPLY_BRIGHTNESS'); @@ -171,6 +173,7 @@ define([ color = sampleAndBlend(\n\ color,\n\ u_dayTextures[' + i + '],\n\ + ' + (typeof(colorPaletteKeys[i]) !== 'undefined' ? 'u_dayTextureColorPalette[' + colorPaletteKeys[i] + ']' : 'u_dayTextures[' + i + ']') + ',\n\ u_dayTextureUseWebMercatorT[' + i + '] ? textureCoordinates.xz : textureCoordinates.xy,\n\ u_dayTextureTexCoordsRectangle[' + i + '],\n\ u_dayTextureTranslationAndScale[' + i + '],\n\ @@ -180,7 +183,8 @@ define([ ' + (applyHue ? 'u_dayTextureHue[' + i + ']' : '0.0') + ',\n\ ' + (applySaturation ? 'u_dayTextureSaturation[' + i + ']' : '0.0') + ',\n\ ' + (applyGamma ? 'u_dayTextureOneOverGamma[' + i + ']' : '0.0') + ',\n\ - ' + (applySplit ? 'u_dayTextureSplit[' + i + ']' : '0.0') + '\n\ + ' + (applySplit ? 'u_dayTextureSplit[' + i + ']' : '0.0') + ',\n\ + ' + (typeof(colorPaletteKeys[i]) !== 'undefined' ? '1' : '0') + '\n\ );\n'; } diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 7bec9532d203..c6203737fb7c 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -755,6 +755,9 @@ define([ u_dayTextureSplit : function() { return this.properties.dayTextureSplit; }, + u_dayTextureColorPalette : function() { + return this.properties.dayTextureColorPalette; + }, // make a separate object so that changes to the properties are seen on // derived commands that combine another uniform map with this one. @@ -780,6 +783,7 @@ define([ dayTextureSaturation : [], dayTextureOneOverGamma : [], dayTextureSplit : [], + dayTextureColorPalette : [], dayIntensity : 0.0, southAndNorthLatitude : new Cartesian2(), @@ -1014,6 +1018,7 @@ define([ do { var numberOfDayTextures = 0; + var numberOfPaletteTextures = 0; var command; var uniformMap; @@ -1075,6 +1080,7 @@ define([ var applyGamma = false; var applyAlpha = false; var applySplit = false; + var colorPaletteKeys = []; while (numberOfDayTextures < maxTextures && imageryIndex < imageryLen) { var tileImagery = tileImageryCollection[imageryIndex]; @@ -1135,6 +1141,12 @@ define([ uniformMapProperties.dayTextureSplit[numberOfDayTextures] = imageryLayer.splitDirection; applySplit = applySplit || uniformMapProperties.dayTextureSplit[numberOfDayTextures] !== 0.0; + if(imageryLayer.colorPalette !== ImageryLayer.DEFAULT_COLOR_PALETTE) { + uniformMapProperties.dayTextureColorPalette[numberOfPaletteTextures] = imageryLayer.colorPalette; + colorPaletteKeys[numberOfDayTextures] = numberOfPaletteTextures; + ++numberOfPaletteTextures; + } + if (defined(imagery.credits)) { var creditDisplay = frameState.creditDisplay; var credits = imagery.credits; @@ -1156,7 +1168,8 @@ define([ uniformMapProperties.minMaxHeight.y = encoding.maximumHeight; Matrix4.clone(encoding.matrix, uniformMapProperties.scaleAndBias); - command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog); + + command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, numberOfPaletteTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, colorPaletteKeys, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog); command.castShadows = castShadows; command.receiveShadows = receiveShadows; command.renderState = renderState; diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js index 9fef8eeac772..727b902c45cd 100644 --- a/Source/Scene/ImageryLayer.js +++ b/Source/Scene/ImageryLayer.js @@ -206,6 +206,14 @@ define([ */ this.splitDirection = defaultValue(options.splitDirection, defaultValue(imageryProvider.defaultSplit, ImageryLayer.DEFAULT_SPLIT)); + /** + * The color palette to apply to this layer. + * + * @type {Array} + * @default false + */ + this.colorPalette = defaultValue(options.colorPalette, defaultValue(imageryProvider.defaultColorPalette, ImageryLayer.DEFAULT_COLOR_PALETTE)); + /** * Determines if this layer is shown. * @@ -311,6 +319,13 @@ define([ */ ImageryLayer.DEFAULT_SPLIT = ImagerySplitDirection.NONE; + /** + * This value is used as the default color palette for the imagery layer if one is not provided during construction + * or by the imagery provider. + * @type {Array} + * @default undefined + */ + ImageryLayer.DEFAULT_COLOR_PALETTE = 1.0; /** * Gets a value indicating whether this layer is the base layer in the * {@link ImageryLayerCollection}. The base layer is the one that underlies all @@ -741,6 +756,22 @@ define([ } } + if(defined(imagery.imageryLayer.colorPalette) && + imagery.imageryLayer.colorPalette !== ImageryLayer.DEFAULT_COLOR_PALETTE && + !(imagery.imageryLayer.colorPalette instanceof Texture) + ) { + imagery.imageryLayer.colorPalette = new Texture({ + context : context, + flipY : false, + source : { + arrayBufferView : imagery.imageryLayer.colorPalette + }, + width : 1, + height : 1024, + pixelFormat : PixelFormat.RGBA + }); + } + // Imagery does not need to be discarded, so upload it to WebGL. var texture; if (defined(image.internalFormat)) { diff --git a/Source/Scene/ImageryProvider.js b/Source/Scene/ImageryProvider.js index 2a0500a0f7b7..bbd5ef89180c 100644 --- a/Source/Scene/ImageryProvider.js +++ b/Source/Scene/ImageryProvider.js @@ -91,6 +91,15 @@ define([ */ this.defaultGamma = undefined; + /** + * The default color palette to apply to this provider. 1.0 uses the unmodified imagery color palette. + * + * @type {Array} + * @default undefined + */ + this.defaultColorPalette = undefined; + + DeveloperError.throwInstantiationError(); } diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index da6c49e2a9d4..925b956f4f76 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -38,6 +38,10 @@ uniform float u_dayTextureOneOverGamma[TEXTURE_UNITS]; uniform vec4 u_dayTextureTexCoordsRectangle[TEXTURE_UNITS]; #endif +#if PALETTE_UNITS > 0 +uniform sampler2D u_dayTextureColorPalette[PALETTE_UNITS]; +#endif + #ifdef SHOW_REFLECTIVE_OCEAN uniform sampler2D u_waterMask; uniform vec4 u_waterMaskTranslationAndScale; @@ -67,6 +71,7 @@ varying vec3 v_mieColor; vec4 sampleAndBlend( vec4 previousColor, sampler2D texture, + sampler2D textureColorPalette, vec2 tileTextureCoordinates, vec4 textureCoordinateRectangle, vec4 textureCoordinateTranslationAndScale, @@ -76,7 +81,8 @@ vec4 sampleAndBlend( float textureHue, float textureSaturation, float textureOneOverGamma, - float split) + float split, + int applyTextureColorPalette) { // This crazy step stuff sets the alpha to 0.0 if this following condition is true: // tileTextureCoordinates.s < textureCoordinateRectangle.s || @@ -110,6 +116,27 @@ vec4 sampleAndBlend( } #endif +#if PALETTE_UNITS > 0 + if(applyTextureColorPalette == 1) { + float step = 1.0/1024.0; + float a1 = texture2D(texture, textureCoordinates).r; + float a2 = texture2D(texture, textureCoordinates + vec2(step, 0.0)).r; + float a3 = texture2D(texture, textureCoordinates + vec2(0.0, step)).r; + float a4 = texture2D(texture, textureCoordinates + vec2(step, step)).r; + if ((a1 != 0.0 && (a2 == 0.0 || a3 == 0.0 || a4 == 0.0)) || a1 == 0.0) { + } else { + vec2 f = fract(textureCoordinates * vec2(1024.0, 1024.0)); + float tA = mix(a1, a2, f.x); + float tB = mix(a3, a4, f.x); + a1 = mix(tA, tB, f.y); + } + + vec4 pixColor = texture2D(textureColorPalette, vec2(0.0, a1)); + color = pixColor.rgb; + alpha = pixColor.a; + } +#endif + #ifdef APPLY_BRIGHTNESS color = mix(vec3(0.0), color, textureBrightness); #endif