diff --git a/CHANGES.md b/CHANGES.md index 03a177007630..f63e15911dbb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,7 @@ _This is an npm-only release to fix a publishing issue_. * `BingMapsImageryProvider` now uses `DiscardEmptyTileImagePolicy` by default to detect missing tiles as zero-length responses instead of inspecting pixel values. [#7810](https://github.com/AnalyticalGraphicsInc/cesium/pull/7810) * Added support for the [AGI_articulations](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/AGI_articulations) vendor extension of glTF 2.0 to the Model primitive graphics API. [#7835](https://github.com/AnalyticalGraphicsInc/cesium/pull/7835) * Reduce the number of Bing transactions and ion Bing sessions used when destroying and recreating the same imagery layer to 1. [#7848](https://github.com/AnalyticalGraphicsInc/cesium/pull/7848) +* Added support for new Mapbox Style API. [#7698](https://github.com/AnalyticalGraphicsInc/cesium/pull/7698) ##### Fixes :wrench: * Fixed an edge case where Cesium would provide ion access token credentials to non-ion servers if the actual asset entrypoint was being hosted by ion. [#7839](https://github.com/AnalyticalGraphicsInc/cesium/pull/7839) diff --git a/Source/Scene/ImageryProvider.js b/Source/Scene/ImageryProvider.js index addce4ab2f09..1a372ddc6a06 100644 --- a/Source/Scene/ImageryProvider.js +++ b/Source/Scene/ImageryProvider.js @@ -31,6 +31,7 @@ define([ * @see GoogleEarthEnterpriseMapsProvider * @see GridImageryProvider * @see MapboxImageryProvider + * @see MapboxStyleImageryProvider * @see SingleTileImageryProvider * @see TileCoordinatesImageryProvider * @see UrlTemplateImageryProvider diff --git a/Source/Scene/MapboxStyleImageryProvider.js b/Source/Scene/MapboxStyleImageryProvider.js new file mode 100644 index 000000000000..753098bbf3c1 --- /dev/null +++ b/Source/Scene/MapboxStyleImageryProvider.js @@ -0,0 +1,371 @@ +define([ + '../Core/Credit', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/DeveloperError', + '../Core/MapboxApi', + '../Core/Resource', + './UrlTemplateImageryProvider' +], function( + Credit, + defaultValue, + defined, + defineProperties, + DeveloperError, + MapboxApi, + Resource, + UrlTemplateImageryProvider) { +'use strict'; + +var trailingSlashRegex = /\/$/; +var defaultCredit = new Credit('© Mapbox © OpenStreetMap Improve this map'); + +/** + * Provides tiled imagery hosted by Mapbox. + * + * @alias MapboxStyleImageryProvider + * @constructor + * + * @param {Object} [options] Object with the following properties: + * @param {Resource|String} [options.url='https://api.mapbox.com/styles/v1/'] The Mapbox server url. + * @param {String} [options.username='mapbox'] The username of the map account. + * @param {String} options.styleId The Mapbox Style ID. + * @param {String} [options.accessToken] The public access token for the imagery. + * @param {Number} [options.tilesize=512] The size of the image tiles. + * @param {Boolean} [options.scaleFactor] Determines if tiles are rendered at a @2x scale factor. + * @param {Ellipsoid} [options.ellipsoid] The ellipsoid. If not specified, the WGS84 ellipsoid is used. + * @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 {Credit|String} [options.credit] A credit for the data source, which is displayed on the canvas. + * + * + * @example + * // Mapbox style provider + * var mapbox = new Cesium.MapboxStyleImageryProvider({ + * styleId: 'streets-v11', + * accessToken: 'thisIsMyAccessToken' + * }); + * + * @see {@link https://docs.mapbox.com/api/maps/#styles} + * @see {@link https://docs.mapbox.com/api/#access-tokens-and-token-scopes} + */ +function MapboxStyleImageryProvider(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var styleId = options.styleId; + //>>includeStart('debug', pragmas.debug); + if (!defined(styleId)) { + throw new DeveloperError('options.styleId is required.'); + } + //>>includeEnd('debug'); + + var url = options.url; + if (!defined(url)) { + url = 'https://api.mapbox.com/styles/v1/'; + } + this._url = url; + + var resource = Resource.createIfNeeded(url); + + var accessToken = MapboxApi.getAccessToken(options.accessToken); + this._styleId = styleId; + this._accessToken = accessToken; + + this._accessTokenErrorCredit = Credit.clone(MapboxApi.getErrorCredit(options.accessToken)); + + var tilesize = defaultValue(options.tilesize, 512); + this._tilesize = tilesize; + + var username = defaultValue(options.username, 'mapbox'); + this._username = username; + + var scaleFactor = defined(options.scaleFactor) ? '@2x' : ''; + + var templateUrl = resource.getUrlComponent(); + if (!trailingSlashRegex.test(templateUrl)) { + templateUrl += '/'; + } + templateUrl += this._username + '/' + styleId + '/tiles/' + this._tilesize + '/{z}/{x}/{y}' + scaleFactor; + resource.url = templateUrl; + + resource.setQueryParameters({ + access_token: accessToken + }); + + var credit; + if (defined(options.credit)) { + credit = options.credit; + if (typeof credit === 'string') { + credit = new Credit(credit); + } + } else { + credit = defaultCredit; + } + + this._resource = resource; + this._imageryProvider = new UrlTemplateImageryProvider({ + url: resource, + credit: credit, + ellipsoid: options.ellipsoid, + minimumLevel: options.minimumLevel, + maximumLevel: options.maximumLevel, + rectangle: options.rectangle + }); +} + +defineProperties(MapboxStyleImageryProvider.prototype, { + /** + * Gets the URL of the Mapbox server. + * @memberof MapboxStyleImageryProvider.prototype + * @type {String} + * @readonly + */ + url : { + get : function() { + return this._url; + } + }, + + /** + * Gets a value indicating whether or not the provider is ready for use. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Boolean} + * @readonly + */ + ready : { + get : function() { + return this._imageryProvider.ready; + } + }, + + /** + * Gets a promise that resolves to true when the provider is ready for use. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Promise.} + * @readonly + */ + readyPromise : { + get : function() { + return this._imageryProvider.readyPromise; + } + }, + + /** + * Gets the rectangle, in radians, of the imagery provided by the instance. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Rectangle} + * @readonly + */ + rectangle: { + get : function() { + return this._imageryProvider.rectangle; + } + }, + + /** + * Gets the width of each tile, in pixels. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Number} + * @readonly + */ + tileWidth : { + get : function() { + return this._imageryProvider.tileWidth; + } + }, + + /** + * Gets the height of each tile, in pixels. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Number} + * @readonly + */ + tileHeight : { + get : function() { + return this._imageryProvider.tileHeight; + } + }, + + /** + * Gets the maximum level-of-detail that can be requested. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Number} + * @readonly + */ + maximumLevel : { + get : function() { + return this._imageryProvider.maximumLevel; + } + }, + + /** + * Gets the minimum level-of-detail that can be requested. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. Generally, + * a minimum level should only be used when the rectangle of the imagery is small + * enough that the number of tiles at the minimum level is small. An imagery + * provider with more than a few tiles at the minimum level will lead to + * rendering problems. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Number} + * @readonly + */ + minimumLevel : { + get : function() { + return this._imageryProvider.minimumLevel; + } + }, + + /** + * Gets the tiling scheme used by the provider. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {TilingScheme} + * @readonly + */ + tilingScheme : { + get : function() { + return this._imageryProvider.tilingScheme; + } + }, + + /** + * 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 MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {TileDiscardPolicy} + * @readonly + */ + tileDiscardPolicy : { + get : function() { + return this._imageryProvider.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 MapboxStyleImageryProvider.prototype + * @type {Event} + * @readonly + */ + errorEvent : { + get : function() { + return this._imageryProvider.errorEvent; + } + }, + + /** + * 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 MapboxStyleImageryProvider#ready} returns true. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Credit} + * @readonly + */ + credit : { + get : function() { + return this._imageryProvider.credit; + } + }, + + /** + * Gets the proxy used by this provider. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Proxy} + * @readonly + */ + proxy : { + get : function() { + return this._imageryProvider.proxy; + } + }, + + /** + * 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. + * @memberof MapboxStyleImageryProvider.prototype + * @type {Boolean} + * @readonly + */ + hasAlphaChannel : { + get : function() { + return this._imageryProvider.hasAlphaChannel; + } + } +}); + +/** + * 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. + */ +MapboxStyleImageryProvider.prototype.getTileCredits = function(x, y, level) { + if (defined(this._accessTokenErrorCredit)) { + return [this._accessTokenErrorCredit]; + } +}; + +/** + * Requests the image for a given tile. This function should + * not be called before {@link MapboxStyleImageryProvider#ready} returns true. + * + * @param {Number} x The tile X coordinate. + * @param {Number} y The tile Y coordinate. + * @param {Number} level The tile level. + * @param {Request} [request] The request object. Intended for internal use only. + * @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. + * + * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. + */ +MapboxStyleImageryProvider.prototype.requestImage = function(x, y, level, request) { + return this._imageryProvider.requestImage(x, y, level, request); +}; + +/** + * 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 MapboxStyleImageryProvider#ready} returns true. + * This function is optional, so it may not exist on all ImageryProviders. + * + * + * @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. + * + * @exception {DeveloperError} pickFeatures must not be called before the imagery provider is ready. + */ +MapboxStyleImageryProvider.prototype.pickFeatures = function(x, y, level, longitude, latitude) { + return this._imageryProvider.pickFeatures(x, y, level, longitude, latitude); +}; + +// Exposed for tests +MapboxStyleImageryProvider._defaultCredit = defaultCredit; + +return MapboxStyleImageryProvider; +}); diff --git a/Source/Widgets/BaseLayerPicker/createDefaultImageryProviderViewModels.js b/Source/Widgets/BaseLayerPicker/createDefaultImageryProviderViewModels.js index ff82277b7ce7..25992e7f4260 100644 --- a/Source/Widgets/BaseLayerPicker/createDefaultImageryProviderViewModels.js +++ b/Source/Widgets/BaseLayerPicker/createDefaultImageryProviderViewModels.js @@ -6,7 +6,7 @@ define([ '../../Scene/createTileMapServiceImageryProvider', '../../Scene/createWorldImagery', '../../Scene/IonImageryProvider', - '../../Scene/MapboxImageryProvider', + '../../Scene/MapboxStyleImageryProvider', '../BaseLayerPicker/ProviderViewModel' ], function( buildModuleUrl, @@ -16,7 +16,7 @@ define([ createTileMapServiceImageryProvider, createWorldImagery, IonImageryProvider, - MapboxImageryProvider, + MapboxStyleImageryProvider, ProviderViewModel) { 'use strict'; @@ -67,8 +67,8 @@ define([ iconUrl: buildModuleUrl('Widgets/Images/ImageryProviders/mapboxSatellite.png'), category : 'Other', creationFunction: function() { - return new MapboxImageryProvider({ - mapId: 'mapbox.satellite' + return new MapboxStyleImageryProvider({ + styleId: 'satellite-v9' }); } })); @@ -79,8 +79,8 @@ define([ iconUrl: buildModuleUrl('Widgets/Images/ImageryProviders/mapboxTerrain.png'), category : 'Other', creationFunction: function() { - return new MapboxImageryProvider({ - mapId: 'mapbox.streets' + return new MapboxStyleImageryProvider({ + styleId: 'satellite-streets-v11' }); } })); @@ -91,8 +91,8 @@ define([ iconUrl: buildModuleUrl('Widgets/Images/ImageryProviders/mapboxStreets.png'), category : 'Other', creationFunction: function() { - return new MapboxImageryProvider({ - mapId: 'mapbox.streets-basic' + return new MapboxStyleImageryProvider({ + styleId: 'streets-v11' }); } })); diff --git a/Specs/Scene/MapboxStyleImageryProviderSpec.js b/Specs/Scene/MapboxStyleImageryProviderSpec.js new file mode 100644 index 000000000000..347c25a804a4 --- /dev/null +++ b/Specs/Scene/MapboxStyleImageryProviderSpec.js @@ -0,0 +1,358 @@ +defineSuite([ + 'Scene/MapboxStyleImageryProvider', + 'Core/Math', + 'Core/Rectangle', + 'Core/RequestScheduler', + 'Core/Resource', + 'Core/WebMercatorTilingScheme', + 'Scene/Imagery', + 'Scene/ImageryLayer', + 'Scene/ImageryProvider', + 'Scene/ImageryState', + 'Specs/pollToPromise' + ], function( + MapboxStyleImageryProvider, + CesiumMath, + Rectangle, + RequestScheduler, + Resource, + WebMercatorTilingScheme, + Imagery, + ImageryLayer, + ImageryProvider, + ImageryState, + pollToPromise) { + 'use strict'; + + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + + afterEach(function() { + Resource._Implementations.createImage = Resource._DefaultImplementations.createImage; + }); + + it('conforms to ImageryProvider interface', function() { + expect(MapboxStyleImageryProvider).toConformToInterface(ImageryProvider); + }); + + it('requires the styleId to be specified', function() { + expect(function() { + return new MapboxStyleImageryProvider({}); + }).toThrowDeveloperError('styleId is required'); + }); + + it('resolves readyPromise', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id' + }); + + return provider.readyPromise.then(function (result) { + expect(result).toBe(true); + expect(provider.ready).toBe(true); + }); + }); + + it('resolves readyPromise with Resource', function() { + var resource = new Resource({ + url : 'made/up/mapbox/server/' + }); + + var provider = new MapboxStyleImageryProvider({ + url : resource, + styleId: 'test-id' + }); + + return provider.readyPromise.then(function (result) { + expect(result).toBe(true); + expect(provider.ready).toBe(true); + }); + }); + + it('returns valid value for hasAlphaChannel', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id' + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + expect(typeof provider.hasAlphaChannel).toBe('boolean'); + }); + }); + + it('supports a slash at the end of the URL', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id' + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).not.toContain('//'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0).then(function(image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + }); + + it('supports no slash at the endof the URL', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server', + styleId: 'test-id' + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).toContain('made/up/mapbox/server/'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0).then(function(image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + }); + + it('requestImage returns a promise for an image and loads it for cross-origin use', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id' + }); + + expect(provider.url).toEqual('made/up/mapbox/server/'); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + expect(provider.tileWidth).toEqual(256); + expect(provider.tileHeight).toEqual(256); + expect(provider.maximumLevel).toBeUndefined(); + expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme); + expect(provider.rectangle).toEqual(new WebMercatorTilingScheme().rectangle); + + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0).then(function(image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + }); + + it('rectangle passed to constructor does not affect tile numbering', function() { + var rectangle = new Rectangle(0.1, 0.2, 0.3, 0.4); + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id', + rectangle : rectangle + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + expect(provider.tileWidth).toEqual(256); + expect(provider.tileHeight).toEqual(256); + expect(provider.maximumLevel).toBeUndefined(); + expect(provider.tilingScheme).toBeInstanceOf(WebMercatorTilingScheme); + expect(provider.rectangle).toEqualEpsilon(rectangle, CesiumMath.EPSILON14); + expect(provider.tileDiscardPolicy).toBeUndefined(); + + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).toContain('/0/0/0'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0).then(function(image) { + expect(Resource._Implementations.createImage).toHaveBeenCalled(); + expect(image).toBeImageOrImageBitmap(); + }); + }); + }); + + it('uses maximumLevel passed to constructor', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id', + maximumLevel : 5 + }); + expect(provider.maximumLevel).toEqual(5); + }); + + it('uses minimumLevel passed to constructor', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id', + minimumLevel : 1 + }); + expect(provider.minimumLevel).toEqual(1); + }); + + it('when no credit is supplied, the provider adds a default credit', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id' + }); + expect(provider.credit).toBe(MapboxStyleImageryProvider._defaultCredit); + }); + + it('turns the supplied credit into a logo', function() { + var creditText = 'Thanks to our awesome made up source of this imagery!'; + var providerWithCredit = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id', + credit: creditText + }); + expect(providerWithCredit.credit.html).toEqual(creditText); + }); + + it('raises error event when image cannot be loaded', function() { + var provider = new MapboxStyleImageryProvider({ + url : 'made/up/mapbox/server/', + styleId: 'test-id' + }); + + var layer = new ImageryLayer(provider); + + var tries = 0; + provider.errorEvent.addEventListener(function(error) { + expect(error.timesRetried).toEqual(tries); + ++tries; + if (tries < 3) { + error.retry = true; + } + setTimeout(function() { + RequestScheduler.update(); + }, 1); + }); + + Resource._Implementations.createImage = function(url, crossOrigin, deferred) { + if (tries === 2) { + // Succeed after 2 tries + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + } else { + // fail + setTimeout(function() { + deferred.reject(); + }, 1); + } + }; + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + var imagery = new Imagery(layer, 0, 0, 0); + imagery.addReference(); + layer._requestImagery(imagery); + RequestScheduler.update(); + + return pollToPromise(function() { + return imagery.state === ImageryState.RECEIVED; + }).then(function() { + expect(imagery.image).toBeImageOrImageBitmap(); + expect(tries).toEqual(2); + imagery.releaseReference(); + }); + }); + }); + + it('contains specified url', function() { + var provider = new MapboxStyleImageryProvider({ + url: 'http://fake.map.com', + styleId: 'test-id' + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).toContain('http://fake.map.com'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0); + }); + }); + + it('contains specified username', function() { + var provider = new MapboxStyleImageryProvider({ + styleId: 'test-id', + username: 'fakeUsername' + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).toContain('https://api.mapbox.com/styles/v1/fakeUsername'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0); + }); + }); + + it('contains specified tilesize', function() { + var provider = new MapboxStyleImageryProvider({ + styleId: 'test-id', + tilesize: 256 + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).toContain('https://api.mapbox.com/styles/v1/mapbox/test-id/tiles/256'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0); + }); + }); + + it('enables @2x scale factor', function() { + var provider = new MapboxStyleImageryProvider({ + styleId: 'test-id', + scaleFactor: true + }); + + return pollToPromise(function() { + return provider.ready; + }).then(function() { + spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) { + expect(url).toContain('https://api.mapbox.com/styles/v1/mapbox/test-id/tiles/512/0/0/0@2x'); + + // Just return any old image. + Resource._DefaultImplementations.createImage('Data/Images/Red16x16.png', crossOrigin, deferred); + }); + + return provider.requestImage(0, 0, 0); + }); + }); +});