diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index f68d9fb0e26..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,40 +0,0 @@ -{ - "extends": "eslint:recommended", - "env": { - "browser": true - }, - "globals": { - "DataView": false, - "ArrayBuffer": false, - "Float32Array": false, - "Float64Array": false, - "Int16Array": false, - "Int32Array": false, - "Int8Array": false, - "Uint16Array": false, - "Uint32Array": false, - "Uint8Array": false, - "Uint8ClampedArray": false - }, - "plugins": [ - "html" - ], - "rules": { - "curly": ["error"], - "eqeqeq": ["error"], - "guard-for-in": ["error"], - "new-cap": ["error", {"properties": false}], - "no-caller": ["error"], - "no-console": "off", - "no-empty": ["error"], - "no-extend-native": ["error"], - "no-extra-boolean-cast": "off", - "no-irregular-whitespace": ["error"], - "no-new": ["error"], - "no-undef": ["error"], - "no-unused-vars": ["error", {"vars": "all", "args": "none"}], - "semi": ["error"], - "strict": ["error"], - "wrap-iife": ["error", "any"] - } -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000000..5559ce5b6e8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": "./Tools/eslint-config-cesium/browser.js", + "rules": { + "no-unused-vars": ["error", {"vars": "all", "args": "none"}] + } +} diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml index 4bb85da06f3..13a58bed92c 100644 --- a/.idea/jsLibraryMappings.xml +++ b/.idea/jsLibraryMappings.xml @@ -1,7 +1,7 @@ - + - + \ No newline at end of file diff --git a/Apps/Sandcastle/.eslintrc b/Apps/Sandcastle/.eslintrc.json similarity index 84% rename from Apps/Sandcastle/.eslintrc rename to Apps/Sandcastle/.eslintrc.json index 2fb0cba37fb..b9795620faa 100644 --- a/Apps/Sandcastle/.eslintrc +++ b/Apps/Sandcastle/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": "../../.eslintrc", + "extends": "../../.eslintrc.json", "globals": { "JSON": true, "require": true, diff --git a/Apps/Sandcastle/gallery/Callback Property.html b/Apps/Sandcastle/gallery/Callback Property.html new file mode 100644 index 00000000000..e4c00147d16 --- /dev/null +++ b/Apps/Sandcastle/gallery/Callback Property.html @@ -0,0 +1,106 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Callback Property.jpg b/Apps/Sandcastle/gallery/Callback Property.jpg new file mode 100644 index 00000000000..3a5cf438d74 Binary files /dev/null and b/Apps/Sandcastle/gallery/Callback Property.jpg differ diff --git a/CHANGES.md b/CHANGES.md index 6fe1c7fc568..95c0637dbb6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,9 +5,14 @@ Change Log * Deprecated * `GoogleEarthImageryProvider` has been deprecated and will be removed in Cesium 1.37, use `GoogleEarthEnterpriseMapsProvider` instead. + * The `throttleRequest` parameter for `TerrainProvider.requestTileGeometry`, `CesiumTerrainProvider.requestTileGeometry`, `VRTheWorldTerrainProvider.requestTileGeometry`, and `EllipsoidTerrainProvider.requestTileGeometry` is deprecated and will be replaced with an optional `Request` object. The `throttleRequests` parameter will be removed in 1.37. Instead to throttle requests set the request's `throttle` property to `true`. + * The ability to provide a Promise for the `options.url` parameter of `loadWithXhr` and for the `url` parameter of `loadArrayBuffer`, `loadBlob`, `loadImageViaBlob`, `loadText`, `loadJson`, `loadXML`, `loadImage`, `loadCRN`, `loadKTX`, and `loadCubeMap` is deprecated. This will be removed in 1.37, instead `url` must be a string. +* Added an `options.request` parameter to `loadWithXhr` and a `request` parameter to `loadArrayBuffer`, `loadBlob`, `loadImageViaBlob`, `loadText`, `loadJson`, `loadJsonp`, `loadXML`, `loadImageFromTypedArray`, `loadImage`, `loadCRN`, and `loadKTX`. * Fixed bug where if polylines were set to follow the surface of an undefined globe, Cesium would crash [#5413] https://github.com/AnalyticalGraphicsInc/cesium/pull/5413 * Fixed a bug where picking clusters would return undefined instead of a list of the clustered entities. [#5286](https://github.com/AnalyticalGraphicsInc/cesium/issues/5286) * Reduced the amount of Sun bloom post-process effect near the horizon. [#5381](https://github.com/AnalyticalGraphicsInc/cesium/issues/5381) +* Updated glTF/glb MIME types. [#5420](https://github.com/AnalyticalGraphicsInc/cesium/issues/5420) +* Fixed a bug where camera zooming worked incorrectly when the display height was greater than the display width [#5421] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5421) * Added Sandcastle demo for ArcticDEM data. [#5224](https://github.com/AnalyticalGraphicsInc/cesium/issues/5224) * `CzmlDataSource` and `KmlDataSource` load functions now take an optional `query` object, which will append query parameters to all network requests. [#5419](https://github.com/AnalyticalGraphicsInc/cesium/pull/5419), [#5434](https://github.com/AnalyticalGraphicsInc/cesium/pull/5434) * Fixed geocoder bug so geocoder can accurately handle NSEW inputs [#5407] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5407) diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index 289fa6b37e1..11fb68504de 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -8,6 +8,7 @@ define([ './defaultValue', './defined', './defineProperties', + './deprecationWarning', './DeveloperError', './Event', './GeographicTilingScheme', @@ -19,8 +20,9 @@ define([ './Math', './OrientedBoundingBox', './QuantizedMeshTerrainData', + './Request', + './RequestType', './TerrainProvider', - './throttleRequestByServer', './TileAvailability', './TileProviderError' ], function( @@ -32,6 +34,7 @@ define([ defaultValue, defined, defineProperties, + deprecationWarning, DeveloperError, Event, GeographicTilingScheme, @@ -43,14 +46,15 @@ define([ CesiumMath, OrientedBoundingBox, QuantizedMeshTerrainData, + Request, + RequestType, TerrainProvider, - throttleRequestByServer, TileAvailability, TileProviderError) { 'use strict'; /** - * A {@link TerrainProvider} that access terrain data in a Cesium terrain format. + * A {@link TerrainProvider} that accesses terrain data in a Cesium terrain format. * The format is described on the * {@link https://github.com/AnalyticalGraphicsInc/cesium/wiki/Cesium-Terrain-Server|Cesium wiki}. * @@ -489,9 +493,8 @@ define([ * @param {Number} x The X coordinate of the tile for which to request geometry. * @param {Number} y The Y coordinate of the tile for which to request geometry. * @param {Number} level The level of the tile for which to request geometry. - * @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. + * * @returns {Promise.|undefined} A promise for the requested geometry. If this method * returns undefined instead of a promise, it is an indication that too many requests are already * pending and the request will be retried later. @@ -499,7 +502,7 @@ define([ * @exception {DeveloperError} This function must not be called before {@link CesiumTerrainProvider#ready} * returns true. */ - CesiumTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) { + CesiumTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug) if (!this._ready) { throw new DeveloperError('requestTileGeometry must not be called before the terrain provider is ready.'); @@ -522,8 +525,6 @@ define([ url = proxy.getURL(url); } - var promise; - var extensionList = []; if (this._requestVertexNormals && this._hasVertexNormals) { extensionList.push(this._littleEndianExtensionSize ? 'octvertexnormals' : 'vertexnormals'); @@ -532,17 +533,19 @@ define([ extensionList.push('watermask'); } - function tileLoader(tileUrl) { - return loadArrayBuffer(tileUrl, getRequestHeader(extensionList)); + if (typeof request === 'boolean') { + deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.37.'); + request = new Request({ + throttle : request, + throttleByServer : request, + type : RequestType.TERRAIN + }); } - throttleRequests = defaultValue(throttleRequests, true); - if (throttleRequests) { - promise = throttleRequestByServer(url, tileLoader); - if (!defined(promise)) { - return undefined; - } - } else { - promise = tileLoader(url); + + var promise = loadArrayBuffer(url, getRequestHeader(extensionList), request); + + if (!defined(promise)) { + return undefined; } var that = this; diff --git a/Source/Core/Ellipsoid.js b/Source/Core/Ellipsoid.js index 6f3eba4bcfc..d31c69aefaf 100644 --- a/Source/Core/Ellipsoid.js +++ b/Source/Core/Ellipsoid.js @@ -1,5 +1,6 @@ /*global define*/ define([ + './Check', './Cartesian3', './Cartographic', './defaultValue', @@ -10,6 +11,7 @@ define([ './Math', './scaleToGeodeticSurface' ], function( + Check, Cartesian3, Cartographic, defaultValue, @@ -27,9 +29,9 @@ define([ z = defaultValue(z, 0.0); //>>includeStart('debug', pragmas.debug); - if (x < 0.0 || y < 0.0 || z < 0.0) { - throw new DeveloperError('All radii components must be greater than or equal to zero.'); - } + Check.typeOf.number.greaterThanOrEquals('x', x, 0.0); + Check.typeOf.number.greaterThanOrEquals('y', y, 0.0); + Check.typeOf.number.greaterThanOrEquals('z', z, 0.0); //>>includeEnd('debug'); ellipsoid._radii = new Cartesian3(x, y, z); @@ -283,12 +285,8 @@ define([ */ Ellipsoid.pack = function(value, array, startingIndex) { //>>includeStart('debug', pragmas.debug); - if (!defined(value)) { - throw new DeveloperError('value is required'); - } - if (!defined(array)) { - throw new DeveloperError('array is required'); - } + Check.typeOf.object('value', value); + Check.defined('array', array); //>>includeEnd('debug'); startingIndex = defaultValue(startingIndex, 0); @@ -308,9 +306,7 @@ define([ */ Ellipsoid.unpack = function(array, startingIndex, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(array)) { - throw new DeveloperError('array is required'); - } + Check.defined('array', array); //>>includeEnd('debug'); startingIndex = defaultValue(startingIndex, 0); @@ -338,9 +334,7 @@ define([ */ Ellipsoid.prototype.geodeticSurfaceNormalCartographic = function(cartographic, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartographic)) { - throw new DeveloperError('cartographic is required.'); - } + Check.typeOf.object('cartographic', cartographic); //>>includeEnd('debug'); var longitude = cartographic.longitude; @@ -422,10 +416,8 @@ define([ */ Ellipsoid.prototype.cartographicArrayToCartesianArray = function(cartographics, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartographics)) { - throw new DeveloperError('cartographics is required.'); - } - //>>includeEnd('debug'); + Check.defined('cartographics', cartographics); + //>>includeEnd('debug') var length = cartographics.length; if (!defined(result)) { @@ -496,9 +488,7 @@ define([ */ Ellipsoid.prototype.cartesianArrayToCartographicArray = function(cartesians, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartesians)) { - throw new DeveloperError('cartesians is required.'); - } + Check.defined('cartesians', cartesians); //>>includeEnd('debug'); var length = cartesians.length; @@ -536,9 +526,7 @@ define([ */ Ellipsoid.prototype.scaleToGeocentricSurface = function(cartesian, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartesian)) { - throw new DeveloperError('cartesian is required.'); - } + Check.typeOf.object('cartesian', cartesian); //>>includeEnd('debug'); if (!defined(result)) { @@ -633,15 +621,13 @@ define([ */ Ellipsoid.prototype.getSurfaceNormalIntersectionWithZAxis = function(position, buffer, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(position)) { - throw new DeveloperError('position is required.'); - } + Check.typeOf.object('position', position); + if (!CesiumMath.equalsEpsilon(this._radii.x, this._radii.y, CesiumMath.EPSILON15)) { throw new DeveloperError('Ellipsoid must be an ellipsoid of revolution (radii.x == radii.y)'); } - if (this._radii.z === 0) { - throw new DeveloperError('Ellipsoid.radii.z must be greater than 0'); - } + + Check.typeOf.number.greaterThan('Ellipsoid.radii.z', this._radii.z, 0); //>>includeEnd('debug'); buffer = defaultValue(buffer, 0.0); diff --git a/Source/Core/EllipsoidTerrainProvider.js b/Source/Core/EllipsoidTerrainProvider.js index c0011c62799..58adf683ca8 100644 --- a/Source/Core/EllipsoidTerrainProvider.js +++ b/Source/Core/EllipsoidTerrainProvider.js @@ -152,14 +152,13 @@ define([ * @param {Number} x The X coordinate of the tile for which to request geometry. * @param {Number} y The Y coordinate of the tile for which to request geometry. * @param {Number} level The level of the tile for which to request geometry. - * @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. + * * @returns {Promise.|undefined} A promise for the requested geometry. If this method * returns undefined instead of a promise, it is an indication that too many requests are already * pending and the request will be retried later. */ - EllipsoidTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) { + EllipsoidTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { var width = 16; var height = 16; return new HeightmapTerrainData({ diff --git a/Source/Core/GoogleEarthEnterpriseMetadata.js b/Source/Core/GoogleEarthEnterpriseMetadata.js index 86bd8cb1570..d5ee5dd6185 100644 --- a/Source/Core/GoogleEarthEnterpriseMetadata.js +++ b/Source/Core/GoogleEarthEnterpriseMetadata.js @@ -13,9 +13,10 @@ define([ './joinUrls', './loadArrayBuffer', './Math', + './Request', + './RequestType', './RuntimeError', - './TaskProcessor', - './throttleRequestByServer' + './TaskProcessor' ], function( dbrootParser, when, @@ -30,9 +31,10 @@ define([ joinUrls, loadArrayBuffer, CesiumMath, + Request, + RequestType, RuntimeError, - TaskProcessor, - throttleRequestByServer) { + TaskProcessor) { 'use strict'; function stringToBuffer(str) { @@ -130,7 +132,7 @@ define([ var that = this; this._readyPromise = requestDbRoot(this) .then(function() { - return that.getQuadTreePacket('', that._quadPacketVersion, false); + return that.getQuadTreePacket('', that._quadPacketVersion); }) .then(function() { return true; @@ -292,30 +294,23 @@ define([ * * @param {String} [quadKey=''] The quadkey to retrieve the packet for. * @param {Number} [version=1] The cnode version to be used in the request. - * @param {Boolean} [throttle=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. * * @private */ - GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket = function(quadKey, version, throttle) { + GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket = function(quadKey, version, request) { version = defaultValue(version, 1); quadKey = defaultValue(quadKey, ''); - throttle = defaultValue(throttle, true); var url = getMetadataUrl(this, quadKey, version); var proxy = this._proxy; if (defined(proxy)) { url = proxy.getURL(url); } - var promise; - if (throttle) { - promise = throttleRequestByServer(url, loadArrayBuffer); - if (!defined(promise)) { - return undefined; - } - } else { - promise = loadArrayBuffer(url); + var promise = loadArrayBuffer(url, undefined, request); + + if (!defined(promise)) { + return undefined; // Throttled } var tileInfo = this._tileInfo; @@ -378,21 +373,18 @@ define([ * @param {Number} x The tile X coordinate. * @param {Number} y The tile Y coordinate. * @param {Number} level The tile level. - * @param {Boolean} [throttle=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. * * @returns {Promise} A promise that resolves to the tile info for the requested quad key * * @private */ - GoogleEarthEnterpriseMetadata.prototype.populateSubtree = function(x, y, level, throttle) { - throttle = defaultValue(throttle, true); + GoogleEarthEnterpriseMetadata.prototype.populateSubtree = function(x, y, level, request) { var quadkey = GoogleEarthEnterpriseMetadata.tileXYToQuadKey(x, y, level); - return populateSubtree(this, quadkey, throttle); + return populateSubtree(this, quadkey, request); }; - function populateSubtree(that, quadKey, throttle) { + function populateSubtree(that, quadKey, request) { var tileInfo = that._tileInfo; var q = quadKey; var t = tileInfo[q]; @@ -406,13 +398,20 @@ define([ t = tileInfo[q]; } + var subtreeRequest; var subtreePromises = that._subtreePromises; var promise = subtreePromises[q]; if (defined(promise)) { return promise .then(function() { - // Recursively call this incase we need multiple subtree requests - return populateSubtree(that, quadKey, throttle); + // Recursively call this in case we need multiple subtree requests + subtreeRequest = new Request({ + throttle : request.throttle, + throttleByServer : request.throttleByServer, + type : request.type, + priorityFunction : request.priorityFunction + }); + return populateSubtree(that, quadKey, subtreeRequest); }); } @@ -427,7 +426,7 @@ define([ // We need to split up the promise here because when will execute syncronously if getQuadTreePacket // is already resolved (like in the tests), so subtreePromises will never get cleared out. // Only the initial request will also remove the promise from subtreePromises. - promise = that.getQuadTreePacket(q, t.cnodeVersion, throttle); + promise = that.getQuadTreePacket(q, t.cnodeVersion, request); if (!defined(promise)) { return undefined; } @@ -435,8 +434,14 @@ define([ return promise .then(function() { - // Recursively call this incase we need multiple subtree requests - return populateSubtree(that, quadKey, throttle); + // Recursively call this in case we need multiple subtree requests + subtreeRequest = new Request({ + throttle : request.throttle, + throttleByServer : request.throttleByServer, + type : request.type, + priorityFunction : request.priorityFunction + }); + return populateSubtree(that, quadKey, subtreeRequest); }) .always(function() { delete subtreePromises[q]; diff --git a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js index 052d1c99af3..d5e2928633b 100644 --- a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js +++ b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js @@ -5,6 +5,7 @@ define([ './defaultValue', './defined', './defineProperties', + './deprecationWarning', './DeveloperError', './Event', './GeographicTilingScheme', @@ -15,9 +16,11 @@ define([ './loadArrayBuffer', './Math', './Rectangle', + './Request', + './RequestState', + './RequestType', './RuntimeError', './TaskProcessor', - './throttleRequestByServer', './TileProviderError' ], function( when, @@ -25,6 +28,7 @@ define([ defaultValue, defined, defineProperties, + deprecationWarning, DeveloperError, Event, GeographicTilingScheme, @@ -35,9 +39,11 @@ define([ loadArrayBuffer, CesiumMath, Rectangle, + Request, + RequestState, + RequestType, RuntimeError, TaskProcessor, - throttleRequestByServer, TileProviderError) { 'use strict'; @@ -152,6 +158,7 @@ define([ this._terrainCache = new TerrainCache(); this._terrainPromises = {}; + this._terrainRequests = {}; this._errorEvent = new Event(); @@ -339,9 +346,7 @@ define([ * @param {Number} x The X coordinate of the tile for which to request geometry. * @param {Number} y The Y coordinate of the tile for which to request geometry. * @param {Number} level The level of the tile for which to request geometry. - * @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. * @returns {Promise.|undefined} A promise for the requested geometry. If this method * returns undefined instead of a promise, it is an indication that too many requests are already * pending and the request will be retried later. @@ -349,7 +354,7 @@ define([ * @exception {DeveloperError} This function must not be called before {@link GoogleEarthEnterpriseProvider#ready} * returns true. */ - GoogleEarthEnterpriseTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) { + GoogleEarthEnterpriseTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug) if (!this._ready) { throw new DeveloperError('requestTileGeometry must not be called before the terrain provider is ready.'); @@ -434,23 +439,31 @@ define([ // Load that terrain var terrainPromises = this._terrainPromises; + var terrainRequests = this._terrainRequests; var url = buildTerrainUrl(this, q, terrainVersion); - var promise; + var sharedPromise; + var sharedRequest; if (defined(terrainPromises[q])) { // Already being loaded possibly from another child, so return existing promise - promise = terrainPromises[q]; + sharedPromise = terrainPromises[q]; + sharedRequest = terrainRequests[q]; } else { // Create new request for terrain - var requestPromise; - throttleRequests = defaultValue(throttleRequests, true); - if (throttleRequests) { - requestPromise = throttleRequestByServer(url, loadArrayBuffer); - if (!defined(requestPromise)) { - return undefined; // Throttled - } - } else { - requestPromise = loadArrayBuffer(url); + if (typeof request === 'boolean') { + deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.37.'); + request = new Request({ + throttle : request, + throttleByServer : request, + type : RequestType.TERRAIN + }); } - promise = requestPromise + sharedRequest = request; + var requestPromise = loadArrayBuffer(url, undefined, sharedRequest); + + if (!defined(requestPromise)) { + return undefined; // Throttled + } + + sharedPromise = requestPromise .then(function(terrain) { if (defined(terrain)) { return taskProcessor.scheduleTask({ @@ -482,22 +495,20 @@ define([ } return when.reject(new RuntimeError('Failed to load terrain.')); - }) - .otherwise(function(error) { - info.terrainState = TerrainState.NONE; - return when.reject(error); }); - terrainPromises[q] = promise; // Store promise without delete from terrainPromises + terrainPromises[q] = sharedPromise; // Store promise without delete from terrainPromises + terrainRequests[q] = sharedRequest; // Set promise so we remove from terrainPromises just one time - promise = promise + sharedPromise = sharedPromise .always(function() { delete terrainPromises[q]; + delete terrainRequests[q]; }); } - return promise + return sharedPromise .then(function() { var buffer = terrainCache.get(quadKey); if (defined(buffer)) { @@ -509,10 +520,17 @@ define([ negativeAltitudeExponentBias: metadata.negativeAltitudeExponentBias, negativeElevationThreshold: metadata.negativeAltitudeThreshold }); - } else { - info.terrainState = TerrainState.NONE; - return when.reject(new RuntimeError('Failed to load terrain.')); } + + return when.reject(new RuntimeError('Failed to load terrain.')); + }) + .otherwise(function(error) { + if (sharedRequest.state === RequestState.CANCELLED) { + request.state = sharedRequest.state; + return when.reject(error); + } + info.terrainState = TerrainState.NONE; + return when.reject(error); }); }; @@ -569,7 +587,12 @@ define([ if (metadata.isValid(quadKey)) { // We will need this tile, so request metadata and return false for now - metadata.populateSubtree(x, y, level); + var request = new Request({ + throttle : true, + throttleByServer : true, + type : RequestType.TERRAIN + }); + metadata.populateSubtree(x, y, level, request); } return false; }; diff --git a/Source/Core/Heap.js b/Source/Core/Heap.js index 166e4fe0f3d..11f379213a3 100644 --- a/Source/Core/Heap.js +++ b/Source/Core/Heap.js @@ -12,202 +12,224 @@ define([ 'use strict'; /** + * Array implementation of a heap. + * * @alias Heap * @constructor * @private * - * @param {Function} comparator The comparator to use for the heap. If comparator(a, b) is less than 0, sort a to a lower index than b, otherwise sort to a higher index. + * @param {Object} options Object with the following properties: + * @param {Heap~ComparatorCallback} options.comparator The comparator to use for the heap. If comparator(a, b) is less than 0, sort a to a lower index than b, otherwise sort to a higher index. */ - function Heap(comparator) { + function Heap(options) { //>>includeStart('debug', pragmas.debug); - Check.defined('comparator', comparator); + Check.typeOf.object('options', options); + Check.defined('options.comparator', options.comparator); //>>includeEnd('debug'); - this._comparator = comparator; - this._data = []; + this._comparator = options.comparator; + this._array = []; this._length = 0; - this._maximumSize = 0; + this._maximumLength = undefined; } defineProperties(Heap.prototype, { /** - * Gets the internal data in the heap. + * Gets the length of the heap. * * @memberof Heap.prototype * - * @type {Array} + * @type {Number} * @readonly */ - data : { + length : { get : function() { - return this._data; + return this._length; } }, /** - * Gets the length of the heap. + * Gets the internal array. * * @memberof Heap.prototype * - * @type {Number} + * @type {Array} * @readonly */ - length : { + internalArray : { get : function() { - return this._length; + return this._array; } }, /** - * Gets or sets the maximum size of the heap. + * Gets and sets the maximum length of the heap. * * @memberof Heap.prototype * * @type {Number} */ - maximumSize : { - get: function() { - return this._maximumSize; + maximumLength : { + get : function() { + return this._maximumLength; }, - - set: function(value) { - this._maximumSize = value; - if (this._length > this._maximumSize && this._maximumSize > 0) { - this._length = this._maximumSize; - this._data.length = this._maximumSize; + set : function(value) { + this._maximumLength = value; + if (this._length > value && value > 0) { + this._length = value; + this._array.length = value; } } + }, + + /** + * The comparator to use for the heap. If comparator(a, b) is less than 0, sort a to a lower index than b, otherwise sort to a higher index. + * + * @memberof Heap.prototype + * + * @type {Heap~ComparatorCallback} + */ + comparator : { + get : function() { + return this._comparator; + } } }); - function swap(data, a, b) { - var temp = data[a]; - data[a] = data[b]; - data[b] = temp; + function swap(array, a, b) { + var temp = array[a]; + array[a] = array[b]; + array[b] = temp; } /** * Resizes the internal array of the heap. * - * @param {Number} [length] The length to resize internal array to. Defaults to the current size of the heap. + * @param {Number} [length] The length to resize internal array to. Defaults to the current length of the heap. */ Heap.prototype.reserve = function(length) { length = defaultValue(length, this._length); - this._data.length = length; + this._array.length = length; }; /** - * Heapify. Update the heap so that index and all descendants satisfy the heap property. + * Update the heap so that index and all descendants satisfy the heap property. * - * @param {Number} index The starting index to heapify from. + * @param {Number} [index=0] The starting index to heapify from. */ Heap.prototype.heapify = function(index) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.number.greaterThanOrEquals('index', index, 0); - //>>includeEnd('debug'); - + index = defaultValue(index, 0); var length = this._length; var comparator = this._comparator; - var data = this._data; + var array = this._array; var candidate = -1; + var inserting = true; - while (true) { // eslint-disable-line no-constant-condition + while (inserting) { var right = 2 * (index + 1); var left = right - 1; - if (left < length && comparator(data[left], data[index]) < 0) { + if (left < length && comparator(array[left], array[index]) < 0) { candidate = left; } else { candidate = index; } - if (right < length && comparator(data[right], data[candidate]) < 0) { + if (right < length && comparator(array[right], array[candidate]) < 0) { candidate = right; } if (candidate !== index) { - swap(data, candidate, index); + swap(array, candidate, index); index = candidate; } else { - break; + inserting = false; } } - - if (this._length > this._maximumSize && this._maximumSize > 0) { - this._length = this._maximumSize; - this._data.length = this._maximumSize; - } }; /** - * Create a heap from an existing array. This will modify the original array. - * - * @param {Array} data The array to convert to a heap. + * Resort the heap. */ - Heap.prototype.buildHeap = function(data) { - //>>includeStart('debug', pragmas.debug); - Check.defined('data', data); - //>>includeEnd('debug'); - - var length = data.length; - this._data = data; - this._length = length; - + Heap.prototype.resort = function() { + var length = this._length; for (var i = Math.ceil(length / 2); i >= 0; --i) { this.heapify(i); } }; /** - * Insert an element into the heap. If the length would grow greater than maximumSize + * Insert an element into the heap. If the length would grow greater than maximumLength * of the heap, extra elements are removed. * - * @param {*} value The element to insert + * @param {*} element The element to insert + * + * @return {*} The element that was removed from the heap if the heap is at full capacity. */ - Heap.prototype.insert = function(value) { + Heap.prototype.insert = function(element) { //>>includeStart('debug', pragmas.debug); - Check.defined('value', value); + Check.defined('element', element); //>>includeEnd('debug'); - var data = this._data; + var array = this._array; var comparator = this._comparator; + var maximumLength = this._maximumLength; var index = this._length++; - if (index < data.length) { - data[index] = value; + if (index < array.length) { + array[index] = element; } else { - data.push(value); + array.push(element); } while (index !== 0) { var parent = Math.floor((index - 1) / 2); - if (comparator(data[index], data[parent]) < 0) { - swap(data, index, parent); + if (comparator(array[index], array[parent]) < 0) { + swap(array, index, parent); index = parent; } else { break; } } - if (this._length > this._maximumSize && this._maximumSize > 0) { - this._length = this._maximumSize; + var removedElement; + + if (defined(maximumLength) && (this._length > maximumLength)) { + removedElement = array[maximumLength]; + this._length = maximumLength; } + + return removedElement; }; /** - * Remove the top element from the heap and return it. + * Remove the element specified by index from the heap and return it. * - * @returns {*} The top element of the heap. + * @param {Number} [index=0] The index to remove. + * @returns {*} The specified element of the heap. */ - Heap.prototype.pop = function() { + Heap.prototype.pop = function(index) { + index = defaultValue(index, 0); if (this._length === 0) { return undefined; } - var data = this._data; - var root = data[0]; - swap(data, 0, --this._length); - this.heapify(0); + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.lessThan('index', index, this._length); + //>>includeEnd('debug'); + + var array = this._array; + var root = array[index]; + swap(array, index, --this._length); + this.heapify(index); return root; }; + /** + * The comparator to use for the heap. + * @callback Heap~ComparatorCallback + * @param {*} a An element in the heap. + * @param {*} b An element in the heap. + * @returns {Number} If the result of the comparison is less than 0, sort a to a lower index than b, otherwise sort to a higher index. + */ + return Heap; }); diff --git a/Source/Core/Request.js b/Source/Core/Request.js index 55873ad1703..42ed344b1c5 100644 --- a/Source/Core/Request.js +++ b/Source/Core/Request.js @@ -1,68 +1,174 @@ /*global define*/ define([ - './defaultValue' + './defaultValue', + './defined', + './defineProperties', + './RequestState', + './RequestType' ], function( - defaultValue) { + defaultValue, + defined, + defineProperties, + RequestState, + RequestType) { 'use strict'; /** - * Stores information for making a request using {@link RequestScheduler}. + * Stores information for making a request. In general this does not need to be constructed directly. * - * @exports Request + * @alias Request + * @constructor * - * @private + * @param {Object} [options] An object with the following properties: + * @param {Boolean} [options.url] The url to request. + * @param {Request~RequestCallback} [options.requestFunction] The function that makes the actual data request. + * @param {Request~CancelCallback} [options.cancelFunction] The function that is called when the request is cancelled. + * @param {Request~PriorityCallback} [options.priorityFunction] The function that is called to update the request's priority, which occurs once per frame. + * @param {Number} [options.priority=0.0] The initial priority of the request. + * @param {Boolean} [options.throttle=false] Whether to throttle and prioritize the request. If false, the request will be sent immediately. If true, the request will be throttled and sent based on priority. + * @param {Boolean} [options.throttleByServer=false] Whether to throttle the request by server. + * @param {RequestType} [options.type=RequestType.OTHER] The type of request. */ function Request(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var throttleByServer = defaultValue(options.throttleByServer, false); + var throttle = throttleByServer || defaultValue(options.throttle, false); + /** * The URL to request. + * + * @type {String} */ this.url = options.url; /** - * Extra parameters to send with the request. For example, HTTP headers or jsonp parameters. + * The function that makes the actual data request. + * + * @type {Request~RequestCallback} + */ + this.requestFunction = options.requestFunction; + + /** + * The function that is called when the request is cancelled. + * + * @type {Request~CancelCallback} */ - this.parameters = options.parameters; + this.cancelFunction = options.cancelFunction; /** - * The actual function that makes the request. + * The function that is called to update the request's priority, which occurs once per frame. + * + * @type {Request~PriorityCallback} */ - this.requestFunction = options.requestFunction; + this.priorityFunction = options.priorityFunction; /** - * Type of request. Used for more fine-grained priority sorting. + * Priority is a unit-less value where lower values represent higher priority. + * For world-based objects, this is usually the distance from the camera. + * A request that does not have a priority function defaults to a priority of 0. + * + * If priorityFunction is defined, this value is updated every frame with the result of that call. + * + * @type {Number} + * @default 0.0 */ - this.type = options.type; + this.priority = defaultValue(options.priority, 0.0); /** - * Specifies that the request should be deferred until an open slot is available. - * A deferred request will always return a promise, which is suitable for data - * sources and utility functions. + * Whether to throttle and prioritize the request. If false, the request will be sent immediately. If true, the + * request will be throttled and sent based on priority. + * + * @type {Boolean} + * @readonly + * + * @default false */ - this.defer = defaultValue(options.defer, false); + this.throttle = throttle; /** - * The distance from the camera, used to prioritize requests. + * Whether to throttle the request by server. Browsers typically support about 6-8 parallel connections + * for HTTP/1 servers, and an unlimited amount of connections for HTTP/2 servers. Setting this value + * to true is preferable for requests going through HTTP/1 servers. + * + * @type {Boolean} + * @readonly + * + * @default false */ - this.distance = defaultValue(options.distance, 0.0); + this.throttleByServer = throttleByServer; - // Helper members for RequestScheduler + /** + * Type of request. + * + * @type {RequestType} + * @readonly + * + * @default RequestType.OTHER + */ + this.type = defaultValue(options.type, RequestType.OTHER); /** - * A promise for when a deferred request can start. + * A key used to identify the server that a request is going to. It is derived from the url's authority and scheme. + * + * @type {String} * * @private */ - this.startPromise = undefined; + this.serverKey = undefined; /** - * Reference to a {@link RequestScheduler~RequestServer}. + * The current state of the request. + * + * @type {RequestState} + * @readonly + */ + this.state = RequestState.UNISSUED; + + /** + * The requests's deferred promise. + * + * @type {Object} + * + * @private + */ + this.deferred = undefined; + + /** + * Whether the request was explicitly cancelled. + * + * @type {Boolean} * * @private */ - this.server = options.server; + this.cancelled = false; } + /** + * Mark the request as cancelled. + * + * @private + */ + Request.prototype.cancel = function() { + this.cancelled = true; + }; + + /** + * The function that makes the actual data request. + * @callback Request~RequestCallback + * @returns {Promise} A promise for the requested data. + */ + + /** + * The function that is called when the request is cancelled. + * @callback Request~CancelCallback + */ + + /** + * The function that is called to update the request's priority, which occurs once per frame. + * @callback Request~PriorityCallback + * @returns {Number} The updated priority value. + */ + return Request; }); diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index b401193e4f5..264838a5b82 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -1,440 +1,424 @@ /*global define*/ define([ - '../ThirdParty/Uri', - '../ThirdParty/when', - './defaultValue', + './clone', + './Check', './defined', - './DeveloperError', + './defineProperties', + './Heap', + './isBlobUri', './isDataUri', - './Queue', - './Request', - './RequestType' + './RequestState', + '../ThirdParty/Uri', + '../ThirdParty/when' ], function( - Uri, - when, - defaultValue, + clone, + Check, defined, - DeveloperError, + defineProperties, + Heap, + isBlobUri, isDataUri, - Queue, - Request, - RequestType) { + RequestState, + Uri, + when) { 'use strict'; - function RequestBudget(request) { - /** - * Total requests allowed this frame. - */ - this.total = 0; + function sortRequests(a, b) { + return a.priority - b.priority; + } - /** - * Total requests used this frame. - */ - this.used = 0; + var statistics = { + numberOfAttemptedRequests : 0, + numberOfActiveRequests : 0, + numberOfCancelledRequests : 0, + numberOfCancelledActiveRequests : 0, + numberOfFailedRequests : 0, + numberOfActiveRequestsEver : 0 + }; - /** - * Server of the request. - */ - this.server = request.server; + var priorityHeapLength = 20; + var requestHeap = new Heap({ + comparator : sortRequests + }); + requestHeap.maximumLength = priorityHeapLength; + requestHeap.reserve(priorityHeapLength); - /** - * Type of request. Used for more fine-grained priority sorting. - */ - this.type = request.type; + var activeRequests = []; + var numberOfActiveRequestsByServer = {}; + + var pageUri = typeof document !== 'undefined' ? new Uri(document.location.href) : new Uri(); + + /** + * Tracks the number of active requests and prioritizes incoming requests. + * + * @exports RequestScheduler + * + * @private + */ + function RequestScheduler() { } /** - * Stores the number of active requests at a particular server. Areas that commonly makes requests may store - * a reference to this object in order to quickly determine whether a request can be issued (e.g. Cesium3DTile). + * The maximum number of simultaneous active requests. Un-throttled requests do not observe this limit. + * @type {Number} + * @default 50 + */ + RequestScheduler.maximumRequests = 50; + + /** + * The maximum number of simultaneous active requests per server. Un-throttled requests do not observe this limit. + * @type {Number} + * @default 6 + */ + RequestScheduler.maximumRequestsPerServer = 6; + + /** + * Specifies if the request scheduler should throttle incoming requests, or let the browser queue requests under its control. + * @type {Boolean} + * @default true */ - function RequestServer(serverName) { + RequestScheduler.throttleRequests = true; + + /** + * When true, log statistics to the console every frame + * @type {Boolean} + * @default false + */ + RequestScheduler.debugShowStatistics = false; + + defineProperties(RequestScheduler, { /** - * Number of active requests at this server. + * Returns the statistics used by the request scheduler. + * + * @memberof RequestScheduler + * + * @type Object + * @readonly */ - this.activeRequests = 0; + statistics : { + get : function() { + return statistics; + } + }, /** - * The name of the server. + * The maximum size of the priority heap. This limits the number of requests that are sorted by priority. Only applies to requests that are not yet active. + * + * @memberof RequestScheduler + * + * @type {Number} + * @default 20 */ - this.serverName = serverName; - } + priorityHeapLength : { + get : function() { + return priorityHeapLength; + }, + set : function(value) { + // If the new length shrinks the heap, need to cancel some of the requests. + // Since this value is not intended to be tweaked regularly it is fine to just cancel the high priority requests. + if (value < priorityHeapLength) { + while (requestHeap.length > value) { + var request = requestHeap.pop(); + cancelRequest(request); + } + } + priorityHeapLength = value; + requestHeap.maximumLength = value; + requestHeap.reserve(value); + } + } + }); - RequestServer.prototype.hasAvailableRequests = function() { - return RequestScheduler.hasAvailableRequests() && (this.activeRequests < RequestScheduler.maximumRequestsPerServer); - }; + function updatePriority(request) { + if (defined(request.priorityFunction)) { + request.priority = request.priorityFunction(); + } + } - RequestServer.prototype.getNumberOfAvailableRequests = function() { - return RequestScheduler.maximumRequestsPerServer - this.activeRequests; - }; + function serverHasOpenSlots(serverKey) { + return numberOfActiveRequestsByServer[serverKey] < RequestScheduler.maximumRequestsPerServer; + } - var activeRequestsByServer = {}; - var activeRequests = 0; - var budgets = []; - var leftoverRequests = []; - var deferredRequests = new Queue(); + function issueRequest(request) { + if (request.state === RequestState.UNISSUED) { + request.state = RequestState.ISSUED; + request.deferred = when.defer(); + } + return request.deferred.promise; + } - var stats = { - numberOfRequestsThisFrame : 0 - }; + function getRequestReceivedFunction(request) { + return function(results) { + if (request.state === RequestState.CANCELLED) { + // If the data request comes back but the request is cancelled, ignore it. + return; + } + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.serverKey]; + request.state = RequestState.RECEIVED; + request.deferred.resolve(results); + }; + } - /** - * Because browsers throttle the number of parallel requests allowed to each server - * and across all servers, this class tracks the number of active requests in progress - * and prioritizes incoming requests. - * - * @exports RequestScheduler - * - * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} - * - * @private - */ - function RequestScheduler() { + function getRequestFailedFunction(request) { + return function(error) { + if (request.state === RequestState.CANCELLED) { + // If the data request comes back but the request is cancelled, ignore it. + return; + } + ++statistics.numberOfFailedRequests; + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.serverKey]; + request.state = RequestState.FAILED; + request.deferred.reject(error); + }; } - function distanceSortFunction(a, b) { - return a.distance - b.distance; + function startRequest(request) { + var promise = issueRequest(request); + request.state = RequestState.ACTIVE; + activeRequests.push(request); + ++statistics.numberOfActiveRequests; + ++statistics.numberOfActiveRequestsEver; + ++numberOfActiveRequestsByServer[request.serverKey]; + request.requestFunction().then(getRequestReceivedFunction(request)).otherwise(getRequestFailedFunction(request)); + return promise; } - function getBudget(request) { - var budget; - var length = budgets.length; - for (var i = 0; i < length; ++i) { - budget = budgets[i]; - if ((budget.server === request.server) && (budget.type === request.type)) { - return budget; - } + function cancelRequest(request) { + var active = request.state === RequestState.ACTIVE; + request.state = RequestState.CANCELLED; + ++statistics.numberOfCancelledRequests; + request.deferred.reject(); + + if (active) { + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.serverKey]; + ++statistics.numberOfCancelledActiveRequests; } - // Not found, create a new budget - budget = new RequestBudget(request); - budgets.push(budget); - return budget; - } - RequestScheduler.resetBudgets = function() { - showStats(); - clearStats(); + if (defined(request.cancelFunction)) { + request.cancelFunction(); + } + } - if (!RequestScheduler.prioritize || !RequestScheduler.throttle) { - return; + /** + * Sort requests by priority and start requests. + */ + RequestScheduler.update = function() { + var i; + var request; + + // Loop over all active requests. Cancelled, failed, or received requests are removed from the array to make room for new requests. + var removeCount = 0; + var activeLength = activeRequests.length; + for (i = 0; i < activeLength; ++i) { + request = activeRequests[i]; + if (request.cancelled) { + // Request was explicitly cancelled + cancelRequest(request); + } + if (request.state !== RequestState.ACTIVE) { + // Request is no longer active, remove from array + ++removeCount; + continue; + } + if (removeCount > 0) { + // Shift back to fill in vacated slots from completed requests + activeRequests[i - removeCount] = request; + } } + activeRequests.length -= removeCount; - // Reset budget totals - var length = budgets.length; - for (var i = 0; i < length; ++i) { - budgets[i].total = 0; - budgets[i].used = 0; + // Update priority of issued requests and resort the heap + var issuedRequests = requestHeap.internalArray; + var issuedLength = requestHeap.length; + for (i = 0; i < issuedLength; ++i) { + updatePriority(issuedRequests[i]); } + requestHeap.resort(); + + // Get the number of open slots and fill with the highest priority requests. + // Un-throttled requests are automatically added to activeRequests, so activeRequests.length may exceed maximumRequests + var openSlots = Math.max(RequestScheduler.maximumRequests - activeRequests.length, 0); + var filledSlots = 0; + while (filledSlots < openSlots && requestHeap.length > 0) { + // Loop until all open slots are filled or the heap becomes empty + request = requestHeap.pop(); + if (request.cancelled) { + // Request was explicitly cancelled + cancelRequest(request); + continue; + } - // Sort all leftover requests by distance - var requests = leftoverRequests; - requests.sort(distanceSortFunction); - - // Allocate new budgets based on the distances of leftover requests - var availableRequests = RequestScheduler.getNumberOfAvailableRequests(); - var requestsLength = requests.length; - for (var j = 0; (j < requestsLength) && (availableRequests > 0); ++j) { - var request = requests[j]; - var budget = getBudget(request); - var budgetAvailable = budget.server.getNumberOfAvailableRequests(); - if (budget.total < budgetAvailable) { - ++budget.total; - --availableRequests; + if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) { + // Open slots are available, but the request is throttled by its server. Cancel and try again later. + cancelRequest(request); + continue; } + + startRequest(request); + ++filledSlots; } - requests.length = 0; + updateStatistics(); }; - var pageUri = typeof document !== 'undefined' ? new Uri(document.location.href) : new Uri(); - /** - * Get the server name from a given url. + * Get the server key from a given url. * * @param {String} url The url. - * @returns {String} The server name. + * @returns {String} The server key. */ - RequestScheduler.getServerName = function(url) { + RequestScheduler.getServerKey = function(url) { //>>includeStart('debug', pragmas.debug); - if (!defined(url)) { - throw new DeveloperError('url is required.'); - } + Check.typeOf.string('url', url); //>>includeEnd('debug'); var uri = new Uri(url).resolve(pageUri); uri.normalize(); - var serverName = uri.authority; - if (!/:/.test(serverName)) { - serverName = serverName + ':' + (uri.scheme === 'https' ? '443' : '80'); + var serverKey = uri.authority; + if (!/:/.test(serverKey)) { + // If the authority does not contain a port number, add port 443 for https or port 80 for http + serverKey = serverKey + ':' + (uri.scheme === 'https' ? '443' : '80'); } - return serverName; - }; - /** - * Get the request server from a given url. - * - * @param {String} url The url. - * @returns {RequestServer} The request server. - */ - RequestScheduler.getRequestServer = function(url) { - var serverName = RequestScheduler.getServerName(url); - var server = activeRequestsByServer[serverName]; - if (!defined(server)) { - server = new RequestServer(serverName); - activeRequestsByServer[serverName] = server; + var length = numberOfActiveRequestsByServer[serverKey]; + if (!defined(length)) { + numberOfActiveRequestsByServer[serverKey] = 0; } - return server; - }; - - /** - * Get the number of available slots at the server pointed to by the url. - * - * @param {String} url The url to check. - * @returns {Number} The number of available slots. - */ - RequestScheduler.getNumberOfAvailableRequestsByServer = function(url) { - return RequestScheduler.getRequestServer(url).getNumberOfAvailableRequests(); - }; - /** - * Get the number of available slots across all servers. - * - * @returns {Number} The number of available slots. - */ - RequestScheduler.getNumberOfAvailableRequests = function() { - return RequestScheduler.maximumRequests - activeRequests; + return serverKey; }; /** - * Checks if there are available slots to make a request at the server pointed to by the url. + * Issue a request. If request.throttle is false, the request is sent immediately. Otherwise the request will be + * queued and sorted by priority before being sent. * - * @param {String} [url] The url to check. - * @returns {Boolean} Returns true if there are available slots, otherwise false. - */ - RequestScheduler.hasAvailableRequestsByServer = function(url) { - return RequestScheduler.getRequestServer(url).hasAvailableRequests(); - }; - - /** - * Checks if there are available slots to make a request, considering the total - * number of available slots across all servers. + * @param {Request} request The request object. * - * @returns {Boolean} Returns true if there are available slots, otherwise false. + * @returns {Promise|undefined} A Promise for the requested data, or undefined if this request does not have high enough priority to be issued. */ - RequestScheduler.hasAvailableRequests = function() { - return activeRequests < RequestScheduler.maximumRequests; - }; - - function requestComplete(request) { - --activeRequests; - --request.server.activeRequests; + RequestScheduler.request = function(request) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('request', request); + Check.typeOf.string('request.url', request.url); + Check.typeOf.func('request.requestFunction', request.requestFunction); + //>>includeEnd('debug'); - // Start a deferred request immediately now that a slot is open - var deferredRequest = deferredRequests.dequeue(); - if (defined(deferredRequest)) { - deferredRequest.startPromise.resolve(deferredRequest); + if (isDataUri(request.url) || isBlobUri(request.url)) { + request.state = RequestState.RECEIVED; + return request.requestFunction(); } - } - function startRequest(request) { - ++activeRequests; - ++request.server.activeRequests; - - return when(request.requestFunction(request.url, request.parameters), function(result) { - requestComplete(request); - return result; - }).otherwise(function(error) { - requestComplete(request); - return when.reject(error); - }); - } + ++statistics.numberOfAttemptedRequests; - function deferRequest(request) { - deferredRequests.enqueue(request); - var deferred = when.defer(); - request.startPromise = deferred; - return deferred.promise.then(startRequest); - } - - function handleLeftoverRequest(request) { - if (RequestScheduler.prioritize) { - leftoverRequests.push(request); + if (!defined(request.serverKey)) { + request.serverKey = RequestScheduler.getServerKey(request.url); } - } - /** - * A function that will make a request if there are available slots to the server. - * Returns undefined immediately if the request would exceed the maximum, allowing - * the caller to retry later instead of queueing indefinitely under the browser's control. - * - * @param {Request} request The request object. - * - * @returns {Promise.|undefined} Either undefined, meaning the request would exceed the maximum number of - * parallel requests, or a Promise for the requested data. - * - * @example - * // throttle requests for an image - * var url = 'http://madeupserver.example.com/myImage.png'; - * var requestFunction = function(url) { - * // in this simple example, loadImage could be used directly as requestFunction. - * return Cesium.loadImage(url); - * }; - * var request = new Request({ - * url : url, - * requestFunction : requestFunction - * }); - * var promise = Cesium.RequestScheduler.schedule(request); - * if (!Cesium.defined(promise)) { - * // too many active requests in progress, try again later. - * } else { - * promise.then(function(image) { - * // handle loaded image - * }); - * } - * - */ - RequestScheduler.schedule = function(request) { - //>>includeStart('debug', pragmas.debug); - if (!defined(request)) { - throw new DeveloperError('request is required.'); - } - if (!defined(request.url)) { - throw new DeveloperError('request.url is required.'); + if (!RequestScheduler.throttleRequests || !request.throttle) { + return startRequest(request); } - if (!defined(request.requestFunction)) { - throw new DeveloperError('request.requestFunction is required.'); - } - //>>includeEnd('debug'); - - ++stats.numberOfRequestsThisFrame; - if (!RequestScheduler.throttle || isDataUri(request.url)) { - return request.requestFunction(request.url, request.parameters); + if (activeRequests.length >= RequestScheduler.maximumRequests) { + // Active requests are saturated. Try again later. + return undefined; } - if (!defined(request.server)) { - request.server = RequestScheduler.getRequestServer(request.url); + if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) { + // Server is saturated. Try again later. + return undefined; } - if (!request.server.hasAvailableRequests()) { - if (!request.defer) { - // No available slots to make the request, return undefined - handleLeftoverRequest(request); - return undefined; - } else { - // If no slots are available, the request is deferred until a slot opens up. - // Return a promise even if the request can't be completed immediately. - return deferRequest(request); - } - } + // Insert into the priority heap and see if a request was bumped off. If this request is the lowest + // priority it will be returned. + updatePriority(request); + var removedRequest = requestHeap.insert(request); - if (RequestScheduler.prioritize && defined(request.type) && !request.defer) { - var budget = getBudget(request); - if (budget.used >= budget.total) { - // Request does not fit in the budget, return undefined - handleLeftoverRequest(request); + if (defined(removedRequest)) { + if (removedRequest === request) { + // Request does not have high enough priority to be issued return undefined; } - ++budget.used; + // A previously issued request has been bumped off the priority heap, so cancel it + cancelRequest(removedRequest); } - return startRequest(request); + return issueRequest(request); }; - /** - * A function that will make a request when an open slot is available. Always returns - * a promise, which is suitable for data sources and utility functions. - * - * @param {String} url The URL to request. - * @param {RequestScheduler~RequestFunction} requestFunction The actual function that - * makes the request. - * @param {Object} [parameters] Extra parameters to send with the request. - * @param {RequestType} [requestType] Type of request. Used for more fine-grained priority sorting. - * - * @returns {Promise.} A Promise for the requested data. - */ - RequestScheduler.request = function(url, requestFunction, parameters, requestType) { - return RequestScheduler.schedule(new Request({ - url : url, - parameters : parameters, - requestFunction : requestFunction, - defer : true, - type : defaultValue(requestType, RequestType.OTHER) - })); - }; - - function clearStats() { - stats.numberOfRequestsThisFrame = 0; + function clearStatistics() { + statistics.numberOfAttemptedRequests = 0; + statistics.numberOfCancelledRequests = 0; + statistics.numberOfCancelledActiveRequests = 0; } - function showStats() { + function updateStatistics() { if (!RequestScheduler.debugShowStatistics) { return; } - if (stats.numberOfRequestsThisFrame > 0) { - console.log('Number of requests attempted: ' + stats.numberOfRequestsThisFrame); + if (statistics.numberOfAttemptedRequests > 0) { + console.log('Number of attempted requests: ' + statistics.numberOfAttemptedRequests); } - - var numberOfActiveRequests = RequestScheduler.maximumRequests - RequestScheduler.getNumberOfAvailableRequests(); - if (numberOfActiveRequests > 0) { - console.log('Number of active requests: ' + numberOfActiveRequests); + if (statistics.numberOfActiveRequests > 0) { + console.log('Number of active requests: ' + statistics.numberOfActiveRequests); + } + if (statistics.numberOfCancelledRequests > 0) { + console.log('Number of cancelled requests: ' + statistics.numberOfCancelledRequests); + } + if (statistics.numberOfCancelledActiveRequests > 0) { + console.log('Number of cancelled active requests: ' + statistics.numberOfCancelledActiveRequests); + } + if (statistics.numberOfFailedRequests > 0) { + console.log('Number of failed requests: ' + statistics.numberOfFailedRequests); } + + clearStatistics(); } /** - * Clears the request scheduler before each spec. + * For testing only. Clears any requests that may not have completed from previous tests. * * @private */ RequestScheduler.clearForSpecs = function() { - activeRequestsByServer = {}; - activeRequests = 0; - budgets = []; - leftoverRequests = []; - deferredRequests = new Queue(); - stats = { - numberOfRequestsThisFrame : 0 - }; + while (requestHeap.length > 0) { + var request = requestHeap.pop(); + cancelRequest(request); + } + var length = activeRequests.length; + for (var i = 0; i < length; ++i) { + cancelRequest(activeRequests[i]); + } + activeRequests.length = 0; + numberOfActiveRequestsByServer = {}; + + // Clear stats + statistics.numberOfAttemptedRequests = 0; + statistics.numberOfActiveRequests = 0; + statistics.numberOfCancelledRequests = 0; + statistics.numberOfCancelledActiveRequests = 0; + statistics.numberOfFailedRequests = 0; + statistics.numberOfActiveRequestsEver = 0; }; /** - * Specifies the maximum number of requests that can be simultaneously open to a single server. If this value is higher than - * the number of requests per server actually allowed by the web browser, Cesium's ability to prioritize requests will be adversely - * affected. - * @type {Number} - * @default 6 - */ - RequestScheduler.maximumRequestsPerServer = 6; - - /** - * Specifies the maximum number of requests that can be simultaneously open for all servers. If this value is higher than - * the number of requests actually allowed by the web browser, Cesium's ability to prioritize requests will be adversely - * affected. - * @type {Number} - * @default 10 - */ - RequestScheduler.maximumRequests = 10; - - /** - * Specifies if the request scheduler should prioritize incoming requests - * @type {Boolean} - * @default true - */ - RequestScheduler.prioritize = true; - - /** - * Specifies if the request scheduler should throttle incoming requests, or let the browser queue requests under its control. - * @type {Boolean} - * @default true + * For testing only. + * + * @private */ - RequestScheduler.throttle = true; + RequestScheduler.numberOfActiveRequestsByServer = function(serverKey) { + return numberOfActiveRequestsByServer[serverKey]; + }; /** - * When true, log statistics to the console every frame - * @type {Boolean} - * @default false + * For testing only. + * + * @private */ - RequestScheduler.debugShowStatistics = false; + RequestScheduler.requestHeap = requestHeap; return RequestScheduler; }); diff --git a/Source/Core/RequestState.js b/Source/Core/RequestState.js new file mode 100644 index 00000000000..23498ef57fc --- /dev/null +++ b/Source/Core/RequestState.js @@ -0,0 +1,64 @@ +/*global define*/ +define([ + '../Core/freezeObject' +], function( + freezeObject) { + 'use strict'; + + /** + * State of the request. + * + * @exports RequestState + */ + var RequestState = { + /** + * Initial unissued state. + * + * @type Number + * @constant + */ + UNISSUED : 0, + + /** + * Issued but not yet active. Will become active when open slots are available. + * + * @type Number + * @constant + */ + ISSUED : 1, + + /** + * Actual http request has been sent. + * + * @type Number + * @constant + */ + ACTIVE : 2, + + /** + * Request completed successfully. + * + * @type Number + * @constant + */ + RECEIVED : 3, + + /** + * Request was cancelled, either explicitly or automatically because of low priority. + * + * @type Number + * @constant + */ + CANCELLED : 4, + + /** + * Request failed. + * + * @type Number + * @constant + */ + FAILED : 5 + }; + + return freezeObject(RequestState); +}); diff --git a/Source/Core/RequestType.js b/Source/Core/RequestType.js index 599ee485963..d01348b54d5 100644 --- a/Source/Core/RequestType.js +++ b/Source/Core/RequestType.js @@ -6,12 +6,41 @@ define([ 'use strict'; /** - * @private + * An enum identifying the type of request. Used for finer grained logging and priority sorting. + * + * @exports RequestType */ var RequestType = { + /** + * Terrain request. + * + * @type Number + * @constant + */ TERRAIN : 0, + + /** + * Imagery request. + * + * @type Number + * @constant + */ IMAGERY : 1, + + /** + * 3D Tiles request. + * + * @type Number + * @constant + */ TILES3D : 2, + + /** + * Other request. + * + * @type Number + * @constant + */ OTHER : 3 }; diff --git a/Source/Core/TerrainProvider.js b/Source/Core/TerrainProvider.js index 5163bae2986..6bdc2e00001 100644 --- a/Source/Core/TerrainProvider.js +++ b/Source/Core/TerrainProvider.js @@ -21,6 +21,7 @@ define([ * * @see EllipsoidTerrainProvider * @see CesiumTerrainProvider + * @see VRTheWorldTerrainProvider */ function TerrainProvider() { DeveloperError.throwInstantiationError(); @@ -197,9 +198,8 @@ define([ * @param {Number} x The X coordinate of the tile for which to request geometry. * @param {Number} y The Y coordinate of the tile for which to request geometry. * @param {Number} level The level of the tile for which to request geometry. - * @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. + * * @returns {Promise.|undefined} A promise for the requested geometry. If this method * returns undefined instead of a promise, it is an indication that too many requests are already * pending and the request will be retried later. diff --git a/Source/Core/VRTheWorldTerrainProvider.js b/Source/Core/VRTheWorldTerrainProvider.js index 4c6524b01f4..5339deb9c6c 100644 --- a/Source/Core/VRTheWorldTerrainProvider.js +++ b/Source/Core/VRTheWorldTerrainProvider.js @@ -5,6 +5,7 @@ define([ './defaultValue', './defined', './defineProperties', + './deprecationWarning', './DeveloperError', './Ellipsoid', './Event', @@ -15,8 +16,9 @@ define([ './loadXML', './Math', './Rectangle', + './Request', + './RequestType', './TerrainProvider', - './throttleRequestByServer', './TileProviderError' ], function( when, @@ -24,6 +26,7 @@ define([ defaultValue, defined, defineProperties, + deprecationWarning, DeveloperError, Ellipsoid, Event, @@ -34,8 +37,9 @@ define([ loadXML, CesiumMath, Rectangle, + Request, + RequestType, TerrainProvider, - throttleRequestByServer, TileProviderError) { 'use strict'; @@ -256,14 +260,12 @@ define([ * @param {Number} x The X coordinate of the tile for which to request geometry. * @param {Number} y The Y coordinate of the tile for which to request geometry. * @param {Number} level The level of the tile for which to request geometry. - * @param {Boolean} [throttleRequests=true] True if the number of simultaneous requests should be limited, - * or false if the request should be initiated regardless of the number of requests - * already in progress. + * @param {Request} [request] The request object. Intended for internal use only. * @returns {Promise.|undefined} A promise for the requested geometry. If this method * returns undefined instead of a promise, it is an indication that too many requests are already * pending and the request will be retried later. */ - VRTheWorldTerrainProvider.prototype.requestTileGeometry = function(x, y, level, throttleRequests) { + VRTheWorldTerrainProvider.prototype.requestTileGeometry = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug); if (!this.ready) { throw new DeveloperError('requestTileGeometry must not be called before ready returns true.'); @@ -278,16 +280,18 @@ define([ url = proxy.getURL(url); } - var promise; + if (typeof request === 'boolean') { + deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.37.'); + request = new Request({ + throttle : request, + throttleByServer : request, + type : RequestType.TERRAIN + }); + } - throttleRequests = defaultValue(throttleRequests, true); - if (throttleRequests) { - promise = throttleRequestByServer(url, loadImage); - if (!defined(promise)) { - return undefined; - } - } else { - promise = loadImage(url); + var promise = loadImage(url, undefined, request); + if (!defined(promise)) { + return undefined; } var that = this; diff --git a/Source/Core/isBlobUri.js b/Source/Core/isBlobUri.js new file mode 100644 index 00000000000..dc4afb78502 --- /dev/null +++ b/Source/Core/isBlobUri.js @@ -0,0 +1,29 @@ +/*global define*/ +define([ + './Check' + ], function( + Check) { + 'use strict'; + + var blobUriRegex = /^blob:/i; + + /** + * Determines if the specified uri is a blob uri. + * + * @exports isBlobUri + * + * @param {String} uri The uri to test. + * @returns {Boolean} true when the uri is a blob uri; otherwise, false. + * + * @private + */ + function isBlobUri(uri) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('uri', uri); + //>>includeEnd('debug'); + + return blobUriRegex.test(uri); + } + + return isBlobUri; +}); diff --git a/Source/Core/isDataUri.js b/Source/Core/isDataUri.js index 116630ced11..0cd24a228c1 100644 --- a/Source/Core/isDataUri.js +++ b/Source/Core/isDataUri.js @@ -1,8 +1,8 @@ /*global define*/ define([ - './defined' + './Check' ], function( - defined) { + Check) { 'use strict'; var dataUriRegex = /^data:/i; @@ -18,11 +18,11 @@ define([ * @private */ function isDataUri(uri) { - if (defined(uri)) { - return dataUriRegex.test(uri); - } + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('uri', uri); + //>>includeEnd('debug'); - return false; + return dataUriRegex.test(uri); } return isDataUri; diff --git a/Source/Core/loadArrayBuffer.js b/Source/Core/loadArrayBuffer.js index ff557505b73..cc56d573ff6 100644 --- a/Source/Core/loadArrayBuffer.js +++ b/Source/Core/loadArrayBuffer.js @@ -13,10 +13,10 @@ define([ * * @exports loadArrayBuffer * - * @param {String|Promise.} url The URL of the binary data, or a promise for the URL. + * @param {String} url The URL of the binary data. * @param {Object} [headers] HTTP headers to send with the requests. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. - * + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * @example * // load a single URL asynchronously @@ -25,15 +25,16 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadArrayBuffer(url, headers) { + function loadArrayBuffer(url, headers, request) { return loadWithXhr({ url : url, responseType : 'arraybuffer', - headers : headers + headers : headers, + request : request }); } diff --git a/Source/Core/loadBlob.js b/Source/Core/loadBlob.js index 7d6ef776d5c..0e136d22c37 100644 --- a/Source/Core/loadBlob.js +++ b/Source/Core/loadBlob.js @@ -13,10 +13,10 @@ define([ * * @exports loadBlob * - * @param {String|Promise.} url The URL of the data, or a promise for the URL. + * @param {String} url The URL of the data. * @param {Object} [headers] HTTP headers to send with the requests. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. - * + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * @example * // load a single URL asynchronously @@ -25,15 +25,16 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadBlob(url, headers) { + function loadBlob(url, headers, request) { return loadWithXhr({ url : url, responseType : 'blob', - headers : headers + headers : headers, + request : request }); } diff --git a/Source/Core/loadCRN.js b/Source/Core/loadCRN.js index f50ebefbfb3..eac7121e100 100644 --- a/Source/Core/loadCRN.js +++ b/Source/Core/loadCRN.js @@ -26,9 +26,10 @@ define([ * * @exports loadCRN * - * @param {String|Promise.|ArrayBuffer} urlOrBuffer The URL of the binary data, a promise for the URL, or an ArrayBuffer. + * @param {String|ArrayBuffer} urlOrBuffer The URL of the binary data or an ArrayBuffer. * @param {Object} [headers] HTTP headers to send with the requests. - * @returns {Promise.} A promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} A promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * @exception {RuntimeError} Unsupported compressed format. * @@ -48,7 +49,7 @@ define([ * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadCRN(urlOrBuffer, headers) { + function loadCRN(urlOrBuffer, headers, request) { //>>includeStart('debug', pragmas.debug); if (!defined(urlOrBuffer)) { throw new DeveloperError('urlOrBuffer is required.'); @@ -59,7 +60,11 @@ define([ if (urlOrBuffer instanceof ArrayBuffer || ArrayBuffer.isView(urlOrBuffer)) { loadPromise = when.resolve(urlOrBuffer); } else { - loadPromise = loadArrayBuffer(urlOrBuffer, headers); + loadPromise = loadArrayBuffer(urlOrBuffer, headers, request); + } + + if (!defined(loadPromise)) { + return undefined; } return loadPromise.then(function(data) { @@ -81,4 +86,4 @@ define([ } return loadCRN; -}); \ No newline at end of file +}); diff --git a/Source/Core/loadImage.js b/Source/Core/loadImage.js index ba299a24c86..79c0c95673a 100644 --- a/Source/Core/loadImage.js +++ b/Source/Core/loadImage.js @@ -1,33 +1,42 @@ /*global define*/ define([ '../ThirdParty/when', + './Check', './defaultValue', './defined', + './deprecationWarning', './DeveloperError', './isCrossOriginUrl', + './isDataUri', + './Request', + './RequestScheduler', './TrustedServers' ], function( when, + Check, defaultValue, defined, + deprecationWarning, DeveloperError, isCrossOriginUrl, + isDataUri, + Request, + RequestScheduler, TrustedServers) { 'use strict'; - var dataUriRegex = /^data:/; - /** * Asynchronously loads the given image URL. Returns a promise that will resolve to * an {@link Image} once loaded, or reject if the image failed to load. * * @exports loadImage * - * @param {String|Promise.} url The source of the image, or a promise for the URL. + * @param {String} url The source URL of the image. * @param {Boolean} [allowCrossOrigin=true] Whether to request the image using Cross-Origin * Resource Sharing (CORS). CORS is only actually used if the image URL is actually cross-origin. * Data URIs are never requested using CORS. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -42,24 +51,37 @@ define([ * when.all([loadImage('image1.png'), loadImage('image2.png')]).then(function(images) { * // images is an array containing all the loaded images * }); - * + * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadImage(url, allowCrossOrigin) { + function loadImage(url, allowCrossOrigin, request) { //>>includeStart('debug', pragmas.debug); - if (!defined(url)) { - throw new DeveloperError('url is required.'); - } + Check.defined('url', url); //>>includeEnd('debug'); allowCrossOrigin = defaultValue(allowCrossOrigin, true); - return when(url, function(url) { + if (typeof url !== 'string') { + // Returning a promise here is okay because it is unlikely that anyone using the deprecated functionality is also + // providing a Request object marked as throttled. + deprecationWarning('url promise', 'url as a Promise is deprecated and will be removed in 1.37'); + return url.then(function(url) { + return makeRequest(url, allowCrossOrigin, request); + }); + } + + return makeRequest(url, allowCrossOrigin, request); + } + + function makeRequest(url, allowCrossOrigin, request) { + request = defined(request) ? request : new Request(); + request.url = url; + request.requestFunction = function() { var crossOrigin; // data URIs can't have allowCrossOrigin set. - if (dataUriRegex.test(url) || !allowCrossOrigin) { + if (isDataUri(url) || !allowCrossOrigin) { crossOrigin = false; } else { crossOrigin = isCrossOriginUrl(url); @@ -70,7 +92,9 @@ define([ loadImage.createImage(url, crossOrigin, deferred); return deferred.promise; - }); + }; + + return RequestScheduler.request(request); } // This is broken out into a separate function so that it can be mocked for testing purposes. diff --git a/Source/Core/loadImageFromTypedArray.js b/Source/Core/loadImageFromTypedArray.js index 9be887ab39c..e7f636031aa 100644 --- a/Source/Core/loadImageFromTypedArray.js +++ b/Source/Core/loadImageFromTypedArray.js @@ -1,11 +1,13 @@ /*global define*/ define([ '../ThirdParty/when', + './Check', './defined', './DeveloperError', './loadImage' ], function( when, + Check, defined, DeveloperError, loadImage) { @@ -14,15 +16,10 @@ define([ /** * @private */ - function loadImageFromTypedArray(uint8Array, format) { + function loadImageFromTypedArray(uint8Array, format, request) { //>>includeStart('debug', pragmas.debug); - if (!defined(uint8Array)) { - throw new DeveloperError('uint8Array is required.'); - } - - if (!defined(format)) { - throw new DeveloperError('format is required.'); - } + Check.typeOf.object('uint8Array', uint8Array); + Check.typeOf.string('format', format); //>>includeEnd('debug'); var blob = new Blob([uint8Array], { @@ -30,7 +27,7 @@ define([ }); var blobUrl = window.URL.createObjectURL(blob); - return loadImage(blobUrl, false).then(function(image) { + return loadImage(blobUrl, false, request).then(function(image) { window.URL.revokeObjectURL(blobUrl); return image; }, function(error) { diff --git a/Source/Core/loadImageViaBlob.js b/Source/Core/loadImageViaBlob.js index 35118d55658..9d2d3292453 100644 --- a/Source/Core/loadImageViaBlob.js +++ b/Source/Core/loadImageViaBlob.js @@ -1,16 +1,18 @@ /*global define*/ define([ '../ThirdParty/when', + './defined', + './isDataUri', './loadBlob', './loadImage' ], function( when, + defined, + isDataUri, loadBlob, loadImage) { 'use strict'; - var dataUriRegex = /^data:/; - /** * Asynchronously loads the given image URL by first downloading it as a blob using * XMLHttpRequest and then loading the image from the buffer via a blob URL. @@ -25,8 +27,9 @@ define([ * * @exports loadImageViaBlob * - * @param {String|Promise.} url The source of the image, or a promise for the URL. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {String} url The source URL of the image. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -42,16 +45,21 @@ define([ * when.all([loadImageViaBlob('image1.png'), loadImageViaBlob('image2.png')]).then(function(images) { * // images is an array containing all the loaded images * }); - * + * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadImageViaBlob(url) { - if (dataUriRegex.test(url)) { - return loadImage(url); + function loadImageViaBlob(url, request) { + if (!xhrBlobSupported || isDataUri(url)) { + return loadImage(url, undefined, request); + } + + var blobPromise = loadBlob(url, undefined, request); + if (!defined(blobPromise)) { + return undefined; } - return loadBlob(url).then(function(blob) { + return blobPromise.then(function(blob) { var blobUrl = window.URL.createObjectURL(blob); return loadImage(blobUrl, false).then(function(image) { @@ -76,5 +84,5 @@ define([ } })(); - return xhrBlobSupported ? loadImageViaBlob : loadImage; + return loadImageViaBlob; }); diff --git a/Source/Core/loadJson.js b/Source/Core/loadJson.js index 01cce7033f1..1fd1de5133c 100644 --- a/Source/Core/loadJson.js +++ b/Source/Core/loadJson.js @@ -26,11 +26,12 @@ define([ * * @exports loadJson * - * @param {String|Promise.} url The URL to request, or a promise for the URL. + * @param {String} url The URL to request. * @param {Object} [headers] HTTP headers to send with the request. * 'Accept: application/json,*/*;q=0.01' is added to the request headers automatically * if not specified. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -39,12 +40,12 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see loadText * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadJson(url, headers) { + function loadJson(url, headers, request) { //>>includeStart('debug', pragmas.debug); if (!defined(url)) { throw new DeveloperError('url is required.'); @@ -59,7 +60,12 @@ define([ headers.Accept = defaultHeaders.Accept; } - return loadText(url, headers).then(function(value) { + var textPromise = loadText(url, headers, request); + if (!defined(textPromise)) { + return undefined; + } + + return textPromise.then(function(value) { return JSON.parse(value); }); } diff --git a/Source/Core/loadJsonp.js b/Source/Core/loadJsonp.js index 730c19b865b..c63f3b58b0b 100644 --- a/Source/Core/loadJsonp.js +++ b/Source/Core/loadJsonp.js @@ -7,7 +7,9 @@ define([ './defined', './DeveloperError', './objectToQuery', - './queryToObject' + './queryToObject', + './Request', + './RequestScheduler' ], function( Uri, when, @@ -16,7 +18,9 @@ define([ defined, DeveloperError, objectToQuery, - queryToObject) { + queryToObject, + Request, + RequestScheduler) { 'use strict'; /** @@ -29,7 +33,8 @@ define([ * @param {Object} [options.parameters] Any extra query parameters to append to the URL. * @param {String} [options.callbackParameterName='callback'] The callback parameter name that the server expects. * @param {Proxy} [options.proxy] A proxy to use for the request. This object is expected to have a getURL function which returns the proxied URL, if needed. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -39,10 +44,10 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadJsonp(url, options) { + function loadJsonp(url, options, request) { //>>includeStart('debug', pragmas.debug); if (!defined(url)) { throw new DeveloperError('url is required.'); @@ -57,19 +62,6 @@ define([ functionName = 'loadJsonp' + Math.random().toString().substring(2, 8); } while (defined(window[functionName])); - var deferred = when.defer(); - - //assign a function with that name in the global scope - window[functionName] = function(data) { - deferred.resolve(data); - - try { - delete window[functionName]; - } catch (e) { - window[functionName] = undefined; - } - }; - var uri = new Uri(url); var queryOptions = queryToObject(defaultValue(uri.query, '')); @@ -90,9 +82,27 @@ define([ url = proxy.getURL(url); } - loadJsonp.loadAndExecuteScript(url, functionName, deferred); + request = defined(request) ? request : new Request(); + request.url = url; + request.requestFunction = function() { + var deferred = when.defer(); + + //assign a function with that name in the global scope + window[functionName] = function(data) { + deferred.resolve(data); + + try { + delete window[functionName]; + } catch (e) { + window[functionName] = undefined; + } + }; + + loadJsonp.loadAndExecuteScript(url, functionName, deferred); + return deferred.promise; + }; - return deferred.promise; + return RequestScheduler.request(request); } // This is broken out into a separate function so that it can be mocked for testing purposes. diff --git a/Source/Core/loadKTX.js b/Source/Core/loadKTX.js index 5baa4b493df..cd8600b10d1 100644 --- a/Source/Core/loadKTX.js +++ b/Source/Core/loadKTX.js @@ -37,9 +37,10 @@ define([ * * @exports loadKTX * - * @param {String|Promise.|ArrayBuffer} urlOrBuffer The URL of the binary data, a promise for the URL, or an ArrayBuffer. + * @param {String|ArrayBuffer} urlOrBuffer The URL of the binary data or an ArrayBuffer. * @param {Object} [headers] HTTP headers to send with the requests. - * @returns {Promise.} A promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} A promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * @exception {RuntimeError} Invalid KTX file. * @exception {RuntimeError} File is the wrong endianness. @@ -69,7 +70,7 @@ define([ * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadKTX(urlOrBuffer, headers) { + function loadKTX(urlOrBuffer, headers, request) { //>>includeStart('debug', pragmas.debug); if (!defined(urlOrBuffer)) { throw new DeveloperError('urlOrBuffer is required.'); @@ -80,7 +81,11 @@ define([ if (urlOrBuffer instanceof ArrayBuffer || ArrayBuffer.isView(urlOrBuffer)) { loadPromise = when.resolve(urlOrBuffer); } else { - loadPromise = loadArrayBuffer(urlOrBuffer, headers); + loadPromise = loadArrayBuffer(urlOrBuffer, headers, request); + } + + if (!defined(loadPromise)) { + return undefined; } return loadPromise.then(function(data) { diff --git a/Source/Core/loadText.js b/Source/Core/loadText.js index 342963ae9b2..8ca386fe1bf 100644 --- a/Source/Core/loadText.js +++ b/Source/Core/loadText.js @@ -13,9 +13,10 @@ define([ * * @exports loadText * - * @param {String|Promise.} url The URL to request, or a promise for the URL. + * @param {String} url The URL to request. * @param {Object} [headers] HTTP headers to send with the request. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -27,15 +28,16 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest|XMLHttpRequest} * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadText(url, headers) { + function loadText(url, headers, request) { return loadWithXhr({ url : url, - headers : headers + headers : headers, + request : request }); } diff --git a/Source/Core/loadWithXhr.js b/Source/Core/loadWithXhr.js index a44debf511d..f5e2690aa77 100644 --- a/Source/Core/loadWithXhr.js +++ b/Source/Core/loadWithXhr.js @@ -1,18 +1,26 @@ /*global define*/ define([ '../ThirdParty/when', + './Check', './defaultValue', './defined', + './deprecationWarning', './DeveloperError', + './Request', './RequestErrorEvent', + './RequestScheduler', './RuntimeError', './TrustedServers' ], function( when, + Check, defaultValue, defined, + deprecationWarning, DeveloperError, + Request, RequestErrorEvent, + RequestScheduler, RuntimeError, TrustedServers) { 'use strict'; @@ -26,13 +34,14 @@ define([ * @exports loadWithXhr * * @param {Object} options Object with the following properties: - * @param {String|Promise.} options.url The URL of the data, or a promise for the URL. + * @param {String} options.url The URL of the data. * @param {String} [options.responseType] The type of response. This controls the type of item returned. * @param {String} [options.method='GET'] The HTTP method to use. * @param {String} [options.data] The data to send with the request, if any. * @param {Object} [options.headers] HTTP headers to send with the request, if any. * @param {String} [options.overrideMimeType] Overrides the MIME type returned by the server. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {Request} [options.request] The request object. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -57,24 +66,45 @@ define([ options = defaultValue(options, defaultValue.EMPTY_OBJECT); //>>includeStart('debug', pragmas.debug); - if (!defined(options.url)) { - throw new DeveloperError('options.url is required.'); - } + Check.defined('options.url', options.url); //>>includeEnd('debug'); + var url = options.url; + + if (typeof url !== 'string') { + // Returning a promise here is okay because it is unlikely that anyone using the deprecated functionality is also + // providing a Request object marked as throttled. + deprecationWarning('url promise', 'options.url as a Promise is deprecated and will be removed in Cesium 1.37'); + return url.then(function(url) { + return makeRequest(options, url); + }); + } + + return makeRequest(options); + } + + function makeRequest(options, url) { var responseType = options.responseType; var method = defaultValue(options.method, 'GET'); var data = options.data; var headers = options.headers; var overrideMimeType = options.overrideMimeType; + url = defaultValue(url, options.url); - return when(options.url, function(url) { + var request = defined(options.request) ? options.request : new Request(); + request.url = url; + request.requestFunction = function() { var deferred = when.defer(); - - loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); - + var xhr = loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); + if (defined(xhr) && defined(xhr.abort)) { + request.cancelFunction = function() { + xhr.abort(); + }; + } return deferred.promise; - }); + }; + + return RequestScheduler.request(request); } var dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/; @@ -192,6 +222,8 @@ define([ }; xhr.send(data); + + return xhr; }; loadWithXhr.defaultLoad = loadWithXhr.load; diff --git a/Source/Core/loadXML.js b/Source/Core/loadXML.js index b9f6915d4e3..780ff6ad5ee 100644 --- a/Source/Core/loadXML.js +++ b/Source/Core/loadXML.js @@ -13,9 +13,10 @@ define([ * * @exports loadXML * - * @param {String|Promise.} url The URL to request, or a promise for the URL. + * @param {String} url The URL to request. * @param {Object} [headers] HTTP headers to send with the request. - * @returns {Promise.} a promise that will resolve to the requested data when loaded. + * @param {Request} [request] The request object. Intended for internal use only. + * @returns {Promise.|undefined} a promise that will resolve to the requested data when loaded. Returns undefined if request.throttle is true and the request does not have high enough priority. * * * @example @@ -27,17 +28,18 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest|XMLHttpRequest} * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} */ - function loadXML(url, headers) { + function loadXML(url, headers, request) { return loadWithXhr({ url : url, responseType : 'document', headers : headers, - overrideMimeType : 'text/xml' + overrideMimeType : 'text/xml', + request : request }); } diff --git a/Source/Core/sampleTerrain.js b/Source/Core/sampleTerrain.js index 909ba8020da..8a43a84b862 100644 --- a/Source/Core/sampleTerrain.js +++ b/Source/Core/sampleTerrain.js @@ -107,7 +107,7 @@ define([ var tilePromises = []; for (i = 0; i < tileRequests.length; ++i) { var tileRequest = tileRequests[i]; - var requestPromise = tileRequest.terrainProvider.requestTileGeometry(tileRequest.x, tileRequest.y, tileRequest.level, false); + var requestPromise = tileRequest.terrainProvider.requestTileGeometry(tileRequest.x, tileRequest.y, tileRequest.level); var tilePromise = when(requestPromise, createInterpolateFunction(tileRequest), createMarkFailedFunction(tileRequest)); tilePromises.push(tilePromise); } diff --git a/Source/Core/throttleRequestByServer.js b/Source/Core/throttleRequestByServer.js deleted file mode 100644 index 14debf754c5..00000000000 --- a/Source/Core/throttleRequestByServer.js +++ /dev/null @@ -1,95 +0,0 @@ -/*global define*/ -define([ - '../ThirdParty/Uri', - '../ThirdParty/when', - './defaultValue' - ], function( - Uri, - when, - defaultValue) { - 'use strict'; - - var activeRequests = {}; - - var pageUri = typeof document !== 'undefined' ? new Uri(document.location.href) : new Uri(); - function getServer(url) { - var uri = new Uri(url).resolve(pageUri); - uri.normalize(); - var server = uri.authority; - if (!/:/.test(server)) { - server = server + ':' + (uri.scheme === 'https' ? '443' : '80'); - } - return server; - } - - /** - * Because browsers throttle the number of parallel requests allowed to each server, - * this function tracks the number of active requests in progress to each server, and - * returns undefined immediately if the request would exceed the maximum, allowing - * the caller to retry later, instead of queueing indefinitely under the browser's control. - * - * @exports throttleRequestByServer - * - * @param {String} url The URL to request. - * @param {throttleRequestByServer~RequestFunction} requestFunction The actual function that - * makes the request. - * @returns {Promise.|undefined} Either undefined, meaning the request would exceed the maximum number of - * parallel requests, or a Promise for the requested data. - * - * - * @example - * // throttle requests for an image - * var url = 'http://madeupserver.example.com/myImage.png'; - * function requestFunction(url) { - * // in this simple example, loadImage could be used directly as requestFunction. - * return Cesium.loadImage(url); - * }; - * var promise = Cesium.throttleRequestByServer(url, requestFunction); - * if (!Cesium.defined(promise)) { - * // too many active requests in progress, try again later. - * } else { - * promise.then(function(image) { - * // handle loaded image - * }); - * } - * - * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} - */ - function throttleRequestByServer(url, requestFunction) { - var server = getServer(url); - - var activeRequestsForServer = defaultValue(activeRequests[server], 0); - if (activeRequestsForServer >= throttleRequestByServer.maximumRequestsPerServer) { - return undefined; - } - - activeRequests[server] = activeRequestsForServer + 1; - - return when(requestFunction(url), function(result) { - activeRequests[server]--; - return result; - }).otherwise(function(error) { - activeRequests[server]--; - return when.reject(error); - }); - } - - /** - * Specifies the maximum number of requests that can be simultaneously open to a single server. If this value is higher than - * the number of requests per server actually allowed by the web browser, Cesium's ability to prioritize requests will be adversely - * affected. - * @type {Number} - * @default 6 - */ - throttleRequestByServer.maximumRequestsPerServer = 6; - - /** - * A function that will make a request if there are available slots to the server. - * @callback throttleRequestByServer~RequestFunction - * - * @param {String} url The url to request. - * @returns {Promise.} A promise for the requested data. - */ - - return throttleRequestByServer; -}); diff --git a/Source/Renderer/loadCubeMap.js b/Source/Renderer/loadCubeMap.js index d576abf3993..81d6528aff5 100644 --- a/Source/Renderer/loadCubeMap.js +++ b/Source/Renderer/loadCubeMap.js @@ -20,7 +20,7 @@ define([ * @exports loadCubeMap * * @param {Context} context The context to use to create the cube map. - * @param {Object} urls The source of each image, or a promise for each URL. See the example below. + * @param {Object} urls The source URL of each image. See the example below. * @param {Boolean} [allowCrossOrigin=true] Whether to request the image using Cross-Origin * Resource Sharing (CORS). CORS is only actually used if the image URL is actually cross-origin. * Data URIs are never requested using CORS. @@ -43,7 +43,7 @@ define([ * }).otherwise(function(error) { * // an error occurred * }); - * + * * @see {@link http://www.w3.org/TR/cors/|Cross-Origin Resource Sharing} * @see {@link http://wiki.commonjs.org/wiki/Promises/A|CommonJS Promises/A} * diff --git a/Source/Scene/ArcGisMapServerImageryProvider.js b/Source/Scene/ArcGisMapServerImageryProvider.js index fd0165ccdde..fa53f26b766 100644 --- a/Source/Scene/ArcGisMapServerImageryProvider.js +++ b/Source/Scene/ArcGisMapServerImageryProvider.js @@ -579,6 +579,7 @@ define([ * @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 @@ -586,7 +587,7 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - ArcGisMapServerImageryProvider.prototype.requestImage = function(x, y, level) { + ArcGisMapServerImageryProvider.prototype.requestImage = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug); if (!this._ready) { throw new DeveloperError('requestImage must not be called before the imagery provider is ready.'); @@ -594,7 +595,7 @@ define([ //>>includeEnd('debug'); var url = buildImageUrl(this, x, y, level); - return ImageryProvider.loadImage(this, url); + return ImageryProvider.loadImage(this, url, request); }; /** diff --git a/Source/Scene/BingMapsImageryProvider.js b/Source/Scene/BingMapsImageryProvider.js index ed6ffc92bad..fdcf1531b7c 100644 --- a/Source/Scene/BingMapsImageryProvider.js +++ b/Source/Scene/BingMapsImageryProvider.js @@ -522,6 +522,7 @@ define([ * @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 @@ -529,7 +530,7 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - BingMapsImageryProvider.prototype.requestImage = function(x, y, level) { + BingMapsImageryProvider.prototype.requestImage = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug); if (!this._ready) { throw new DeveloperError('requestImage must not be called before the imagery provider is ready.'); @@ -537,7 +538,7 @@ define([ //>>includeEnd('debug'); var url = buildImageUrl(this, x, y, level); - return ImageryProvider.loadImage(this, url); + return ImageryProvider.loadImage(this, url, request); }; /** diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index 5e3180a55f5..029b8dfc6ad 100644 --- a/Source/Scene/Camera.js +++ b/Source/Scene/Camera.js @@ -608,14 +608,17 @@ define([ if (directionChanged || transformChanged) { camera._directionWC = Matrix4.multiplyByPointAsVector(transform, direction, camera._directionWC); + Cartesian3.normalize(camera._directionWC, camera._directionWC); } if (upChanged || transformChanged) { camera._upWC = Matrix4.multiplyByPointAsVector(transform, up, camera._upWC); + Cartesian3.normalize(camera._upWC, camera._upWC); } if (rightChanged || transformChanged) { camera._rightWC = Matrix4.multiplyByPointAsVector(transform, right, camera._rightWC); + Cartesian3.normalize(camera._rightWC, camera._rightWC); } if (positionChanged || directionChanged || upChanged || rightChanged || transformChanged) { @@ -1817,30 +1820,57 @@ define([ } //>>includeEnd('debug'); + var ratio; amount = amount * 0.5; - var newRight = frustum.right - amount; - var newLeft = frustum.left + amount; - var maxRight = camera._maxCoord.x; - if (camera._scene.mapMode2D === MapMode2D.ROTATE) { - maxRight *= camera.maximumZoomFactor; - } + if((Math.abs(frustum.top) + Math.abs(frustum.bottom)) > (Math.abs(frustum.left) + Math.abs(frustum.right))) { + var newTop = frustum.top - amount; + var newBottom = frustum.bottom + amount; - if (newRight > maxRight) { - newRight = maxRight; - newLeft = -maxRight; - } + var maxBottom = camera._maxCoord.y; + if (camera._scene.mapMode2D === MapMode2D.ROTATE) { + maxBottom *= camera.maximumZoomFactor; + } - if (newRight <= newLeft) { - newRight = 1.0; - newLeft = -1.0; - } + if (newBottom > maxBottom) { + newBottom = maxBottom; + newTop = -maxBottom; + } + + if (newTop <= newBottom) { + newTop = 1.0; + newBottom = -1.0; + } + + ratio = frustum.right / frustum.top; + frustum.top = newTop; + frustum.bottom = newBottom; + frustum.right = frustum.top * ratio; + frustum.left = -frustum.right; + } else { + var newRight = frustum.right - amount; + var newLeft = frustum.left + amount; - var ratio = frustum.top / frustum.right; - frustum.right = newRight; - frustum.left = newLeft; - frustum.top = frustum.right * ratio; - frustum.bottom = -frustum.top; + var maxRight = camera._maxCoord.x; + if (camera._scene.mapMode2D === MapMode2D.ROTATE) { + maxRight *= camera.maximumZoomFactor; + } + + if (newRight > maxRight) { + newRight = maxRight; + newLeft = -maxRight; + } + + if (newRight <= newLeft) { + newRight = 1.0; + newLeft = -1.0; + } + ratio = frustum.top / frustum.right; + frustum.right = newRight; + frustum.left = newLeft; + frustum.top = frustum.right * ratio; + frustum.bottom = -frustum.top; + } } function zoom3D(camera, amount) { diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index d386273ab93..1e27a4df4f8 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -237,10 +237,39 @@ define([ } }; + function createTileBoundingRegion(tile) { + var minimumHeight; + var maximumHeight; + if (defined(tile.parent) && defined(tile.parent.data)) { + minimumHeight = tile.parent.data.minimumHeight; + maximumHeight = tile.parent.data.maximumHeight; + } + return new TileBoundingRegion({ + rectangle : tile.rectangle, + ellipsoid : tile.tilingScheme.ellipsoid, + minimumHeight : minimumHeight, + maximumHeight : maximumHeight + }); + } + + function createPriorityFunction(surfaceTile, frameState) { + return function() { + return surfaceTile.tileBoundingRegion.distanceToCamera(frameState); + }; + } + GlobeSurfaceTile.processStateMachine = function(tile, frameState, terrainProvider, imageryLayerCollection, vertexArraysToDestroy) { var surfaceTile = tile.data; if (!defined(surfaceTile)) { surfaceTile = tile.data = new GlobeSurfaceTile(); + // Create the TileBoundingRegion now in order to estimate the distance, which is used to prioritize the request. + // Since the terrain isn't loaded yet, estimate the heights using its parent's values. + surfaceTile.tileBoundingRegion = createTileBoundingRegion(tile); + } + + if (!defined(tile._priorityFunction)) { + // The priority function is used to prioritize requests among all requested tiles + tile._priorityFunction = createPriorityFunction(surfaceTile, frameState); } if (tile.state === QuadtreeTileLoadState.START) { @@ -307,6 +336,7 @@ define([ if (isDoneLoading) { tile.state = QuadtreeTileLoadState.DONE; + tile._priorityFunction = undefined; } } }; @@ -339,7 +369,7 @@ define([ var suspendUpsampling = false; if (defined(loaded)) { - loaded.processLoadStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level); + loaded.processLoadStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level, tile._priorityFunction); // Publish the terrain data on the tile as soon as it is available. // We'll potentially need it to upsample child tiles. diff --git a/Source/Scene/GoogleEarthEnterpriseImageryProvider.js b/Source/Scene/GoogleEarthEnterpriseImageryProvider.js index 962914509b2..3dfb87bbcf2 100644 --- a/Source/Scene/GoogleEarthEnterpriseImageryProvider.js +++ b/Source/Scene/GoogleEarthEnterpriseImageryProvider.js @@ -13,8 +13,9 @@ define([ '../Core/loadImageFromTypedArray', '../Core/Math', '../Core/Rectangle', + '../Core/Request', + '../Core/RequestType', '../Core/RuntimeError', - '../Core/throttleRequestByServer', '../Core/TileProviderError', '../ThirdParty/protobuf-minimal', '../ThirdParty/when' @@ -32,8 +33,9 @@ define([ loadImageFromTypedArray, CesiumMath, Rectangle, + Request, + RequestType, RuntimeError, - throttleRequestByServer, TileProviderError, protobuf, when) { @@ -430,6 +432,7 @@ define([ * @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 @@ -437,7 +440,7 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - GoogleEarthEnterpriseImageryProvider.prototype.requestImage = function(x, y, level) { + GoogleEarthEnterpriseImageryProvider.prototype.requestImage = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug); if (!this._ready) { throw new DeveloperError('requestImage must not be called before the imagery provider is ready.'); @@ -450,7 +453,13 @@ define([ var info = metadata.getTileInformation(x, y, level); if (!defined(info)) { if (metadata.isValid(quadKey)) { - metadata.populateSubtree(x, y, level); + var metadataRequest = new Request({ + throttle : request.throttle, + throttleByServer : request.throttleByServer, + type : request.type, + priorityFunction : request.priorityFunction + }); + metadata.populateSubtree(x, y, level, metadataRequest); return undefined; // No metadata so return undefined so we can be loaded later } else { return invalidImage; // Image doesn't exist @@ -463,9 +472,9 @@ define([ } // Load the var url = buildImageUrl(this, info, x, y, level); - var promise = throttleRequestByServer(url, loadArrayBuffer); + var promise = loadArrayBuffer(url, undefined, request); if (!defined(promise)) { - return undefined; //Throttled + return undefined; // Throttled } return promise diff --git a/Source/Scene/GoogleEarthEnterpriseMapsProvider.js b/Source/Scene/GoogleEarthEnterpriseMapsProvider.js index a1657c86ded..4251d3d2d40 100644 --- a/Source/Scene/GoogleEarthEnterpriseMapsProvider.js +++ b/Source/Scene/GoogleEarthEnterpriseMapsProvider.js @@ -538,6 +538,7 @@ define([ * @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 @@ -545,7 +546,7 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - GoogleEarthEnterpriseMapsProvider.prototype.requestImage = function(x, y, level) { + GoogleEarthEnterpriseMapsProvider.prototype.requestImage = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug); if (!this._ready) { throw new DeveloperError('requestImage must not be called before the imagery provider is ready.'); @@ -553,7 +554,7 @@ define([ //>>includeEnd('debug'); var url = buildImageUrl(this, x, y, level); - return ImageryProvider.loadImage(this, url); + return ImageryProvider.loadImage(this, url, request); }; /** diff --git a/Source/Scene/GoogleEarthImageryProvider.js b/Source/Scene/GoogleEarthImageryProvider.js index 7f1e076533c..cca47c60bf3 100644 --- a/Source/Scene/GoogleEarthImageryProvider.js +++ b/Source/Scene/GoogleEarthImageryProvider.js @@ -11,7 +11,7 @@ define([ * Provides tiled imagery using the Google Earth Imagery API. * * Notes: This imagery provider was deprecated in Cesium 1.35 and replaced with {@link GoogleEarthEnterpriseMapsProvider}. - * These are for use with the 2D Maps API. For 3D Earth API uses, see {@link GoogleEarthEngerpriseImageryProvider}. + * These are for use with the 2D Maps API. For 3D Earth API uses, see {@link GoogleEarthEnterpriseImageryProvider}. * GoogleEarthImageryProvider will be removed in Cesium 1.37. * * @alias GoogleEarthImageryProvider diff --git a/Source/Scene/GridImageryProvider.js b/Source/Scene/GridImageryProvider.js index 779449aa53b..7bfa4f16485 100644 --- a/Source/Scene/GridImageryProvider.js +++ b/Source/Scene/GridImageryProvider.js @@ -321,12 +321,13 @@ define([ * @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. */ - GridImageryProvider.prototype.requestImage = function(x, y, level) { + GridImageryProvider.prototype.requestImage = function(x, y, level, request) { return this._canvas; }; diff --git a/Source/Scene/Imagery.js b/Source/Scene/Imagery.js index 607fb1319cc..07414e2ac3e 100644 --- a/Source/Scene/Imagery.js +++ b/Source/Scene/Imagery.js @@ -2,10 +2,12 @@ define([ '../Core/defined', '../Core/destroyObject', + '../Core/RequestState', './ImageryState' ], function( defined, destroyObject, + RequestState, ImageryState) { 'use strict'; @@ -20,6 +22,7 @@ define([ this.x = x; this.y = y; this.level = level; + this.request = undefined; if (level !== 0) { var parentX = x / 2 | 0; @@ -84,10 +87,10 @@ define([ return this.referenceCount; }; - Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection) { + Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, priorityFunction) { if (this.state === ImageryState.UNLOADED) { this.state = ImageryState.TRANSITIONING; - this.imageryLayer._requestImagery(this); + this.imageryLayer._requestImagery(this, priorityFunction); } if (this.state === ImageryState.RECEIVED) { diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js index 9fef8eeac77..39d7014322f 100644 --- a/Source/Scene/ImageryLayer.js +++ b/Source/Scene/ImageryLayer.js @@ -12,6 +12,9 @@ define([ '../Core/Math', '../Core/PixelFormat', '../Core/Rectangle', + '../Core/Request', + '../Core/RequestState', + '../Core/RequestType', '../Core/TerrainProvider', '../Core/TileProviderError', '../Core/WebMercatorProjection', @@ -49,6 +52,9 @@ define([ CesiumMath, PixelFormat, Rectangle, + Request, + RequestState, + RequestType, TerrainProvider, TileProviderError, WebMercatorProjection, @@ -656,8 +662,9 @@ define([ * @private * * @param {Imagery} imagery The imagery to request. + * @param {Function} [priorityFunction] The priority function used for sorting the imagery request. */ - ImageryLayer.prototype._requestImagery = function(imagery) { + ImageryLayer.prototype._requestImagery = function(imagery, priorityFunction) { var imageryProvider = this._imageryProvider; var that = this; @@ -669,14 +676,23 @@ define([ imagery.image = image; imagery.state = ImageryState.RECEIVED; + imagery.request = undefined; TileProviderError.handleSuccess(that._requestImageError); } function failure(e) { + if (imagery.request.state === RequestState.CANCELLED) { + // Cancelled due to low priority - try again later. + imagery.state = ImageryState.UNLOADED; + imagery.request = undefined; + return; + } + // Initially assume failure. handleError may retry, in which case the state will // change to TRANSITIONING. imagery.state = ImageryState.FAILED; + imagery.request = undefined; var message = 'Failed to obtain image tile X: ' + imagery.x + ' Y: ' + imagery.y + ' Level: ' + imagery.level + '.'; that._requestImageError = TileProviderError.handleError( @@ -690,12 +706,20 @@ define([ } function doRequest() { + var request = new Request({ + throttle : true, + throttleByServer : true, + type : RequestType.IMAGERY, + priorityFunction : priorityFunction + }); + imagery.request = request; imagery.state = ImageryState.TRANSITIONING; - var imagePromise = imageryProvider.requestImage(imagery.x, imagery.y, imagery.level); + var imagePromise = imageryProvider.requestImage(imagery.x, imagery.y, imagery.level, request); if (!defined(imagePromise)) { // Too many parallel requests, so postpone loading tile. imagery.state = ImageryState.UNLOADED; + imagery.request = undefined; return; } diff --git a/Source/Scene/ImageryProvider.js b/Source/Scene/ImageryProvider.js index 9d52032c6c1..36c5a2eb573 100644 --- a/Source/Scene/ImageryProvider.js +++ b/Source/Scene/ImageryProvider.js @@ -6,8 +6,7 @@ define([ '../Core/loadCRN', '../Core/loadImage', '../Core/loadImageViaBlob', - '../Core/loadKTX', - '../Core/throttleRequestByServer' + '../Core/loadKTX' ], function( defined, defineProperties, @@ -15,8 +14,7 @@ define([ loadCRN, loadImage, loadImageViaBlob, - loadKTX, - throttleRequestByServer) { + loadKTX) { 'use strict'; /** @@ -268,6 +266,7 @@ define([ * @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 @@ -308,20 +307,22 @@ define([ * * @param {ImageryProvider} imageryProvider The imagery provider for the URL. * @param {String} url The URL of the image. + * @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. */ - ImageryProvider.loadImage = function(imageryProvider, url) { + ImageryProvider.loadImage = function(imageryProvider, url, request) { if (ktxRegex.test(url)) { - return throttleRequestByServer(url, loadKTX); + return loadKTX(url, undefined, request); } else if (crnRegex.test(url)) { - return throttleRequestByServer(url, loadCRN); + return loadCRN(url, undefined, request); } else if (defined(imageryProvider.tileDiscardPolicy)) { - return throttleRequestByServer(url, loadImageViaBlob); + return loadImageViaBlob(url, request); } - return throttleRequestByServer(url, loadImage); + + return loadImage(url, undefined, request); }; return ImageryProvider; diff --git a/Source/Scene/MapboxImageryProvider.js b/Source/Scene/MapboxImageryProvider.js index 4e725a2023f..ac2949df0c5 100644 --- a/Source/Scene/MapboxImageryProvider.js +++ b/Source/Scene/MapboxImageryProvider.js @@ -317,6 +317,7 @@ define([ * @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 @@ -324,8 +325,8 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - MapboxImageryProvider.prototype.requestImage = function(x, y, level) { - return this._imageryProvider.requestImage(x, y, level); + MapboxImageryProvider.prototype.requestImage = function(x, y, level, request) { + return this._imageryProvider.requestImage(x, y, level, request); }; /** diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 2b18b81397a..01f858ad35c 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -156,8 +156,8 @@ define([ FAILED : 3 }; - // GLTF_SPEC: Figure out correct mime types (https://github.com/KhronosGroup/glTF/issues/412) - var defaultModelAccept = 'model/vnd.gltf.binary,model/vnd.gltf+json,model/gltf.binary,model/gltf+json;q=0.8,application/json;q=0.2,*/*;q=0.01'; + // glTF MIME types discussed in https://github.com/KhronosGroup/glTF/issues/412 and https://github.com/KhronosGroup/glTF/issues/943 + var defaultModelAccept = 'model/gltf-binary,model/gltf+json;q=0.8,application/json;q=0.2,*/*;q=0.01'; function LoadResources() { this.vertexBuffersToCreate = new Queue(); diff --git a/Source/Scene/QuadtreePrimitive.js b/Source/Scene/QuadtreePrimitive.js index c5b73611277..faab9989d1f 100644 --- a/Source/Scene/QuadtreePrimitive.js +++ b/Source/Scene/QuadtreePrimitive.js @@ -100,7 +100,6 @@ define([ this._tileLoadQueueLow = []; // low priority tiles were refined past or are non-visible parts of quads. this._tileReplacementQueue = new TileReplacementQueue(); this._levelZeroTiles = undefined; - this._levelZeroTilesReady = false; this._loadQueueTimeSlice = 5.0; this._addHeightCallbacks = []; diff --git a/Source/Scene/QuadtreeTile.js b/Source/Scene/QuadtreeTile.js index 5ebea3fe005..65c45f7274a 100644 --- a/Source/Scene/QuadtreeTile.js +++ b/Source/Scene/QuadtreeTile.js @@ -67,6 +67,7 @@ define([ // distance - for example, by using the natural ordering of a quadtree. // QuadtreePrimitive gets/sets this private property. this._distance = 0.0; + this._priorityFunction = undefined; this._customData = []; this._frameUpdated = undefined; diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index ac453266528..2489e3a3d19 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -2587,7 +2587,6 @@ define([ scene._preRender.raiseEvent(scene, time); scene._jobScheduler.resetBudgets(); - RequestScheduler.resetBudgets(); var context = scene.context; var us = context.uniformState; @@ -2654,6 +2653,7 @@ define([ } context.endFrame(); + RequestScheduler.update(); callAfterRenderFunctions(frameState); scene._postRender.raiseEvent(scene, time); diff --git a/Source/Scene/SingleTileImageryProvider.js b/Source/Scene/SingleTileImageryProvider.js index 69b70830a52..9e869c6b5b6 100644 --- a/Source/Scene/SingleTileImageryProvider.js +++ b/Source/Scene/SingleTileImageryProvider.js @@ -370,6 +370,7 @@ define([ * @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 @@ -377,7 +378,7 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - SingleTileImageryProvider.prototype.requestImage = function(x, y, level) { + SingleTileImageryProvider.prototype.requestImage = function(x, y, level, request) { //>>includeStart('debug', pragmas.debug); if (!this._ready) { throw new DeveloperError('requestImage must not be called before the imagery provider is ready.'); diff --git a/Source/Scene/TileCoordinatesImageryProvider.js b/Source/Scene/TileCoordinatesImageryProvider.js index 35531470ac0..0ecf383b481 100644 --- a/Source/Scene/TileCoordinatesImageryProvider.js +++ b/Source/Scene/TileCoordinatesImageryProvider.js @@ -240,12 +240,13 @@ define([ * @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. */ - TileCoordinatesImageryProvider.prototype.requestImage = function(x, y, level) { + TileCoordinatesImageryProvider.prototype.requestImage = function(x, y, level, request) { var canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js index a38510ebae2..d3ffdd839df 100644 --- a/Source/Scene/TileImagery.js +++ b/Source/Scene/TileImagery.js @@ -50,7 +50,7 @@ define([ var loadingImagery = this.loadingImagery; var imageryLayer = loadingImagery.imageryLayer; - loadingImagery.processStateMachine(frameState, !this.useWebMercatorT); + loadingImagery.processStateMachine(frameState, !this.useWebMercatorT, tile._priorityFunction); if (loadingImagery.state === ImageryState.READY) { if (defined(this.readyImagery)) { @@ -92,7 +92,7 @@ define([ // Push the ancestor's load process along a bit. This is necessary because some ancestor imagery // tiles may not be attached directly to a terrain tile. Such tiles will never load if // we don't do it here. - closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT); + closestAncestorThatNeedsLoading.processStateMachine(frameState, !this.useWebMercatorT, tile._priorityFunction); return false; // not done loading } else { // This imagery tile is failed or invalid, and we have the "best available" substitute. diff --git a/Source/Scene/TileTerrain.js b/Source/Scene/TileTerrain.js index d97b629b6ef..98c315518c9 100644 --- a/Source/Scene/TileTerrain.js +++ b/Source/Scene/TileTerrain.js @@ -6,13 +6,15 @@ define([ '../Core/DeveloperError', '../Core/IndexDatatype', '../Core/OrientedBoundingBox', + '../Core/Request', + '../Core/RequestState', + '../Core/RequestType', '../Core/TileProviderError', '../Renderer/Buffer', '../Renderer/BufferUsage', '../Renderer/VertexArray', '../ThirdParty/when', - './TerrainState', - './TileBoundingRegion' + './TerrainState' ], function( BoundingSphere, Cartesian3, @@ -20,13 +22,15 @@ define([ DeveloperError, IndexDatatype, OrientedBoundingBox, + Request, + RequestState, + RequestType, TileProviderError, Buffer, BufferUsage, VertexArray, when, - TerrainState, - TileBoundingRegion) { + TerrainState) { 'use strict'; /** @@ -52,6 +56,7 @@ define([ this.mesh = undefined; this.vertexArray = undefined; this.upsampleDetails = upsampleDetails; + this.request = undefined; } TileTerrain.prototype.freeResources = function() { @@ -83,18 +88,14 @@ define([ surfaceTile.maximumHeight = mesh.maximumHeight; surfaceTile.boundingSphere3D = BoundingSphere.clone(mesh.boundingSphere3D, surfaceTile.boundingSphere3D); surfaceTile.orientedBoundingBox = OrientedBoundingBox.clone(mesh.orientedBoundingBox, surfaceTile.orientedBoundingBox); - surfaceTile.tileBoundingRegion = new TileBoundingRegion({ - rectangle : tile.rectangle, - minimumHeight : mesh.minimumHeight, - maximumHeight : mesh.maximumHeight, - ellipsoid : tile.tilingScheme.ellipsoid - }); + surfaceTile.tileBoundingRegion.minimumHeight = mesh.minimumHeight; + surfaceTile.tileBoundingRegion.maximumHeight = mesh.maximumHeight; tile.data.occludeePointInScaledSpace = Cartesian3.clone(mesh.occludeePointInScaledSpace, surfaceTile.occludeePointInScaledSpace); }; - TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level) { + TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level, priorityFunction) { if (this.state === TerrainState.UNLOADED) { - requestTileGeometry(this, terrainProvider, x, y, level); + requestTileGeometry(this, terrainProvider, x, y, level, priorityFunction); } if (this.state === TerrainState.RECEIVED) { @@ -106,16 +107,26 @@ define([ } }; - function requestTileGeometry(tileTerrain, terrainProvider, x, y, level) { + function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, priorityFunction) { function success(terrainData) { tileTerrain.data = terrainData; tileTerrain.state = TerrainState.RECEIVED; + tileTerrain.request = undefined; } function failure() { + if (tileTerrain.request.state === RequestState.CANCELLED) { + // Cancelled due to low priority - try again later. + tileTerrain.data = undefined; + tileTerrain.state = TerrainState.UNLOADED; + tileTerrain.request = undefined; + return; + } + // Initially assume failure. handleError may retry, in which case the state will // change to RECEIVING or UNLOADED. tileTerrain.state = TerrainState.FAILED; + tileTerrain.request = undefined; var message = 'Failed to obtain terrain tile X: ' + x + ' Y: ' + y + ' Level: ' + level + '.'; terrainProvider._requestError = TileProviderError.handleError( @@ -129,17 +140,24 @@ define([ function doRequest() { // Request the terrain from the terrain provider. - tileTerrain.data = terrainProvider.requestTileGeometry(x, y, level); + var request = new Request({ + throttle : true, + throttleByServer : true, + type : RequestType.TERRAIN, + priorityFunction : priorityFunction + }); + tileTerrain.request = request; + tileTerrain.data = terrainProvider.requestTileGeometry(x, y, level, request); // If the request method returns undefined (instead of a promise), the request // has been deferred. if (defined(tileTerrain.data)) { tileTerrain.state = TerrainState.RECEIVING; - when(tileTerrain.data, success, failure); } else { // Deferred - try again later. tileTerrain.state = TerrainState.UNLOADED; + tileTerrain.request = undefined; } } diff --git a/Source/Scene/UrlTemplateImageryProvider.js b/Source/Scene/UrlTemplateImageryProvider.js index aa7aee267f0..6d1edf87cca 100644 --- a/Source/Scene/UrlTemplateImageryProvider.js +++ b/Source/Scene/UrlTemplateImageryProvider.js @@ -265,7 +265,6 @@ define([ } }, - /** * Gets the URL template to use to use to pick features. If this property is not specified, * {@link UrlTemplateImageryProvider#pickFeatures} will immediately returned undefined, indicating no @@ -602,19 +601,20 @@ define([ * @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. */ - UrlTemplateImageryProvider.prototype.requestImage = function(x, y, level) { + UrlTemplateImageryProvider.prototype.requestImage = function(x, y, level, request) { //>>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); - return ImageryProvider.loadImage(this, url); + return ImageryProvider.loadImage(this, url, request); }; /** @@ -669,8 +669,8 @@ define([ return loadText(url).then(format.callback).otherwise(doRequest); } else { return loadWithXhr({ - url: url, - responseType: format.format + url : url, + responseType : format.format }).then(handleResponse.bind(undefined, format)).otherwise(doRequest); } } diff --git a/Source/Scene/WebMapServiceImageryProvider.js b/Source/Scene/WebMapServiceImageryProvider.js index 3f76e7622ae..93e98bc4a01 100644 --- a/Source/Scene/WebMapServiceImageryProvider.js +++ b/Source/Scene/WebMapServiceImageryProvider.js @@ -433,6 +433,7 @@ define([ * @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 @@ -440,8 +441,8 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - WebMapServiceImageryProvider.prototype.requestImage = function(x, y, level) { - return this._tileProvider.requestImage(x, y, level); + WebMapServiceImageryProvider.prototype.requestImage = function(x, y, level, request) { + return this._tileProvider.requestImage(x, y, level, request); }; /** diff --git a/Source/Scene/WebMapTileServiceImageryProvider.js b/Source/Scene/WebMapTileServiceImageryProvider.js index 624f1123fcf..21986a61866 100644 --- a/Source/Scene/WebMapTileServiceImageryProvider.js +++ b/Source/Scene/WebMapTileServiceImageryProvider.js @@ -432,6 +432,7 @@ define([ * @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 @@ -439,9 +440,9 @@ define([ * * @exception {DeveloperError} requestImage must not be called before the imagery provider is ready. */ - WebMapTileServiceImageryProvider.prototype.requestImage = function(x, y, level) { + WebMapTileServiceImageryProvider.prototype.requestImage = function(x, y, level, request) { var url = buildImageUrl(this, x, y, level); - return ImageryProvider.loadImage(this, url); + return ImageryProvider.loadImage(this, url, request); }; /** diff --git a/Specs/.eslintrc b/Specs/.eslintrc.json similarity index 56% rename from Specs/.eslintrc rename to Specs/.eslintrc.json index c9b69aa5b6b..297a41b8f96 100644 --- a/Specs/.eslintrc +++ b/Specs/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": "../.eslintrc", + "extends": "../.eslintrc.json", "env": { "jasmine": true } diff --git a/Specs/Core/CesiumTerrainProviderSpec.js b/Specs/Core/CesiumTerrainProviderSpec.js index 9238b813569..04cb24bff55 100644 --- a/Specs/Core/CesiumTerrainProviderSpec.js +++ b/Specs/Core/CesiumTerrainProviderSpec.js @@ -9,6 +9,8 @@ defineSuite([ 'Core/loadWithXhr', 'Core/Math', 'Core/QuantizedMeshTerrainData', + 'Core/Request', + 'Core/RequestScheduler', 'Core/TerrainProvider', 'Specs/pollToPromise', 'ThirdParty/when' @@ -22,11 +24,17 @@ defineSuite([ loadWithXhr, CesiumMath, QuantizedMeshTerrainData, + Request, + RequestScheduler, TerrainProvider, pollToPromise, when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadWithXhr.load = loadWithXhr.defaultLoad; }); @@ -84,6 +92,12 @@ defineSuite([ }); } + function createRequest() { + return new Request({ + throttleByServer : true + }); + } + it('conforms to TerrainProvider interface', function() { expect(CesiumTerrainProvider).toConformToInterface(TerrainProvider); }); @@ -575,15 +589,15 @@ defineSuite([ return pollToPromise(function() { return terrainProvider.ready; }).then(function() { - var promise = terrainProvider.requestTileGeometry(0, 0, 0); - expect(promise).toBeDefined(); - + var promise; var i; - for (i = 0; i < 10; ++i) { - promise = terrainProvider.requestTileGeometry(0, 0, 0); + for (i = 0; i < RequestScheduler.maximumRequestsPerServer; ++i) { + promise = terrainProvider.requestTileGeometry(0, 0, 0, createRequest()); } + RequestScheduler.update(); + expect(promise).toBeDefined(); - promise = terrainProvider.requestTileGeometry(0, 0, 0); + promise = terrainProvider.requestTileGeometry(0, 0, 0, createRequest()); expect(promise).toBeUndefined(); for (i = 0; i < deferreds.length; ++i) { diff --git a/Specs/Core/GoogleEarthEnterpriseMetadataSpec.js b/Specs/Core/GoogleEarthEnterpriseMetadataSpec.js index f1ce40b3498..d3b2237c40a 100644 --- a/Specs/Core/GoogleEarthEnterpriseMetadataSpec.js +++ b/Specs/Core/GoogleEarthEnterpriseMetadataSpec.js @@ -7,6 +7,7 @@ defineSuite([ 'Core/defaultValue', 'Core/loadWithXhr', 'Core/Math', + 'Core/Request', 'ThirdParty/when' ], function( GoogleEarthEnterpriseMetadata, @@ -16,6 +17,7 @@ defineSuite([ defaultValue, loadWithXhr, CesiumMath, + Request, when) { 'use strict'; @@ -114,7 +116,7 @@ defineSuite([ it('populateSubtree', function() { var quad = '0123'; var index = 0; - spyOn(GoogleEarthEnterpriseMetadata.prototype, 'getQuadTreePacket').and.callFake(function(quadKey, version) { + spyOn(GoogleEarthEnterpriseMetadata.prototype, 'getQuadTreePacket').and.callFake(function(quadKey, version, request) { quadKey = defaultValue(quadKey, '') + index.toString(); this._tileInfo[quadKey] = new GoogleEarthEnterpriseTileInformation(0xFF, 1, 1, 1); index = (index + 1) % 4; @@ -125,17 +127,20 @@ defineSuite([ var metadata = new GoogleEarthEnterpriseMetadata({ url : 'http://test.server' }); + var request = new Request({ + throttle : true + }); return metadata.readyPromise .then(function() { var tileXY = GoogleEarthEnterpriseMetadata.quadKeyToTileXY(quad); - return metadata.populateSubtree(tileXY.x, tileXY.y, tileXY.level); + return metadata.populateSubtree(tileXY.x, tileXY.y, tileXY.level, request); }) .then(function() { expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket.calls.count()).toEqual(4); - expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('', 1, false); - expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('0', 1, true); - expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('01', 1, true); - expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('012', 1, true); + expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('', 1); + expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('0', 1, request); + expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('01', 1, request); + expect(GoogleEarthEnterpriseMetadata.prototype.getQuadTreePacket).toHaveBeenCalledWith('012', 1, request); var tileInfo = metadata._tileInfo; expect(tileInfo['0']).toBeDefined(); diff --git a/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js b/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js index 00d2ca6d731..9917d46c18b 100644 --- a/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js +++ b/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js @@ -12,6 +12,8 @@ defineSuite([ 'Core/loadImage', 'Core/loadWithXhr', 'Core/Math', + 'Core/Request', + 'Core/RequestScheduler', 'Core/TerrainProvider', 'Specs/pollToPromise', 'ThirdParty/when' @@ -28,6 +30,8 @@ defineSuite([ loadImage, loadWithXhr, CesiumMath, + Request, + RequestScheduler, TerrainProvider, pollToPromise, when) { @@ -74,6 +78,16 @@ defineSuite([ }); } + function createRequest() { + return new Request({ + throttleByServer : true + }); + } + + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadWithXhr.load = loadWithXhr.defaultLoad; }); @@ -304,17 +318,15 @@ defineSuite([ }); }) .then(function() { - var promise = terrainProvider.requestTileGeometry(1, 2, 3); - expect(promise).toBeDefined(); - return promise; - }) - .then(function(terrainData) { - expect(terrainData).toBeDefined(); - for (var i = 0; i < 10; ++i) { - promises.push(terrainProvider.requestTileGeometry(i, i, i)); + var promise; + for (var i = 0; i < RequestScheduler.maximumRequestsPerServer; ++i) { + promise = terrainProvider.requestTileGeometry(i, i, i, createRequest()); + promises.push(promise); } + RequestScheduler.update(); + expect(promise).toBeDefined(); - return terrainProvider.requestTileGeometry(1, 2, 3); + return terrainProvider.requestTileGeometry(1, 2, 3, createRequest()); }) .then(function(terrainData) { expect(terrainData).toBeUndefined(); diff --git a/Specs/Core/HeapSpec.js b/Specs/Core/HeapSpec.js index 209324d5c74..4d41e7e48fb 100644 --- a/Specs/Core/HeapSpec.js +++ b/Specs/Core/HeapSpec.js @@ -8,16 +8,17 @@ defineSuite([ var length = 100; function checkHeap(heap, comparator) { - var data = heap.data; + var array = heap.internalArray; var pass = true; - for (var i = 0; i < heap.length; ++i) { + var length = heap.length; + for (var i = 0; i < length; ++i) { var left = 2 * (i + 1) - 1; var right = 2 * (i + 1); if (left < heap.length) { - pass = pass && (comparator(data[i], data[left]) <= 0); + pass = pass && (comparator(array[i], array[left]) <= 0); } if (right < heap.length) { - pass = pass && (comparator(data[i], data[right]) <= 0); + pass = pass && (comparator(array[i], array[right]) <= 0); } } return pass; @@ -29,17 +30,22 @@ defineSuite([ } it('maintains heap property on insert', function() { - var heap = new Heap(comparator); + var heap = new Heap({ + comparator : comparator + }); var pass = true; for (var i = 0; i < length; ++i) { heap.insert(Math.random()); pass = pass && checkHeap(heap, comparator); } + expect(pass).toBe(true); }); it('maintains heap property on pop', function() { - var heap = new Heap(comparator); + var heap = new Heap({ + comparator : comparator + }); var i; for (i = 0; i < length; ++i) { heap.insert(Math.random()); @@ -52,32 +58,26 @@ defineSuite([ expect(pass).toBe(true); }); - it('can build heap', function() { - var heap = new Heap(comparator); - var arr = new Array(length); - for (var i = 0; i < length; ++i) { - arr[i] = Math.random(); - } - heap.buildHeap(arr); - expect(checkHeap(heap, comparator)).toBe(true); - }); - - it('limited by maximum size', function() { - var heap = new Heap(comparator); - heap.maximumSize = length / 2; + it('limited by maximum length', function() { + var heap = new Heap({ + comparator : comparator + }); + heap.maximumLength = length / 2; var pass = true; for (var i = 0; i < length; ++i) { heap.insert(Math.random()); pass = pass && checkHeap(heap, comparator); } expect(pass).toBe(true); - expect(heap.length <= heap.maximumSize).toBe(true); + expect(heap.length <= heap.maximumLength).toBe(true); // allowed one extra slot for swapping - expect(heap.data.length <= heap.maximumSize + 1).toBe(true); + expect(heap.internalArray.length).toBeLessThanOrEqualTo(heap.maximumLength + 1); }); it('pops in sorted order', function() { - var heap = new Heap(comparator); + var heap = new Heap({ + comparator : comparator + }); var i; for (i = 0; i < length; ++i) { heap.insert(Math.random()); @@ -91,4 +91,85 @@ defineSuite([ } expect(pass).toBe(true); }); + + it('insert returns the removed element when maximumLength is set', function() { + var heap = new Heap({ + comparator : comparator + }); + heap.maximumLength = length; + + var i; + var max = 0.0; + var min = 1.0; + var values = new Array(length); + for (i = 0; i < length; ++i) { + var value = Math.random(); + max = Math.max(max, value); + min = Math.min(min, value); + values[i] = value; + } + + // Push 99 values + for (i = 0; i < length - 1; ++i) { + heap.insert(values[i]); + } + + // Push 100th, nothing is removed so it returns undefined + var removed = heap.insert(values[length - 1]); + expect(removed).toBeUndefined(); + + // Insert value, an element is removed + removed = heap.insert(max - 0.1); + expect(removed).toBeDefined(); + + // If this value is the least priority it will be returned + removed = heap.insert(max + 0.1); + expect(removed).toBe(max + 0.1); + }); + + it('resort', function() { + function comparator(a, b) { + return a.distance - b.distance; + } + + var i; + var heap = new Heap({ + comparator : comparator + }); + for (i = 0; i < length; ++i) { + heap.insert({ + distance : i / (length - 1), + id : i + }); + } + + // Check that elements are initially sorted + var element; + var elements = []; + var currentId = 0; + while (heap.length > 0) { + element = heap.pop(); + elements.push(element); + expect(element.id).toBeGreaterThanOrEqualTo(currentId); + currentId = element.id; + } + + // Add back into heap + for (i = 0; i < length; ++i) { + heap.insert(elements[i]); + } + + // Invert priority + for (i = 0; i < length; ++i) { + elements[i].distance = 1.0 - elements[i].distance; + } + + // Resort and check the the elements are popped in the opposite order now + heap.resort(); + while (heap.length > 0) { + element = heap.pop(); + expect(element.id).toBeLessThanOrEqualTo(currentId); + currentId = element.id; + } + }); }); diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index b9fc5485334..4dfff78ecff 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -2,37 +2,46 @@ defineSuite([ 'Core/RequestScheduler', 'Core/Request', + 'Core/RequestState', 'Core/RequestType', 'ThirdParty/when' ], function( RequestScheduler, Request, + RequestState, RequestType, when) { 'use strict'; var originalMaximumRequests; var originalMaximumRequestsPerServer; + var originalPriorityHeapLength; - beforeEach(function() { + beforeAll(function() { originalMaximumRequests = RequestScheduler.maximumRequests; originalMaximumRequestsPerServer = RequestScheduler.maximumRequestsPerServer; + originalPriorityHeapLength = RequestScheduler.priorityHeapLength; + }); + + beforeEach(function() { + RequestScheduler.clearForSpecs(); }); afterEach(function() { RequestScheduler.maximumRequests = originalMaximumRequests; RequestScheduler.maximumRequestsPerServer = originalMaximumRequestsPerServer; + RequestScheduler.priorityHeapLength = originalPriorityHeapLength; }); - it('schedule throws when request is undefined', function() { + it('request throws when request is undefined', function() { expect(function() { - RequestScheduler.schedule(); + RequestScheduler.request(); }).toThrowDeveloperError(); }); - it('schedule throws when request.url is undefined', function() { + it('request throws when request.url is undefined', function() { expect(function() { - RequestScheduler.schedule(new Request({ + RequestScheduler.request(new Request({ requestFunction : function(url) { return undefined; } @@ -40,57 +49,94 @@ defineSuite([ }).toThrowDeveloperError(); }); - it('schedule throws when request.requestFunction is undefined', function() { + it('request throws when request.requestFunction is undefined', function() { expect(function() { - RequestScheduler.schedule(new Request({ + RequestScheduler.request(new Request({ url : 'file/path' })); }).toThrowDeveloperError(); }); + it('getServer throws if url is undefined', function() { + expect(function() { + RequestScheduler.getServerKey(); + }).toThrowDeveloperError(); + }); + + it('getServer with https', function() { + var server = RequestScheduler.getServerKey('https://foo.com/1'); + expect(server).toEqual('foo.com:443'); + }); + + it('getServer with http', function() { + var server = RequestScheduler.getServerKey('http://foo.com/1'); + expect(server).toEqual('foo.com:80'); + }); + it('honors maximumRequests', function() { RequestScheduler.maximumRequests = 2; + var statistics = RequestScheduler.statistics; var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } - var request = new Request({ - url : 'http://foo.com/1', - requestFunction : requestFunction - }); + function createRequest() { + return new Request({ + url : 'http://foo.com/1', + requestFunction : requestFunction, + throttle : true + }); + } + + var promise1 = RequestScheduler.request(createRequest()); + var promise2 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); - var promise1 = RequestScheduler.schedule(request); - var promise2 = RequestScheduler.schedule(request); - var promise3 = RequestScheduler.schedule(request); + // Scheduler is full, promise3 will be undefined + var promise3 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); - expect(deferreds.length).toBe(2); + expect(statistics.numberOfActiveRequests).toBe(2); expect(promise1).toBeDefined(); expect(promise2).toBeDefined(); expect(promise3).not.toBeDefined(); + // Scheduler now has an empty slot, promise4 goes through deferreds[0].resolve(); + RequestScheduler.update(); + + expect(statistics.numberOfActiveRequests).toBe(1); + + var promise4 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); - var promise4 = RequestScheduler.schedule(request); - expect(deferreds.length).toBe(3); + expect(statistics.numberOfActiveRequests).toBe(2); expect(promise4).toBeDefined(); - var promise5 = RequestScheduler.schedule(request); - expect(deferreds.length).toBe(3); + // Scheduler is full, promise5 will be undefined + var promise5 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); + + expect(statistics.numberOfActiveRequests).toBe(2); expect(promise5).not.toBeDefined(); + // maximumRequests increases, promise6 goes through RequestScheduler.maximumRequests = 3; - var promise6 = RequestScheduler.schedule(request); - expect(deferreds.length).toBe(4); + var promise6 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); + + expect(statistics.numberOfActiveRequests).toBe(3); expect(promise6).toBeDefined(); - deferreds[1].resolve(); - deferreds[2].resolve(); - deferreds[3].resolve(); + var length = deferreds.length; + for (var i = 0; i < length; ++i) { + deferreds[i].resolve(); + } }); it('honors maximumRequestsPerServer', function() { @@ -98,357 +144,561 @@ defineSuite([ var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } - var request = new Request({ - url : 'http://foo.com/1', - requestFunction : requestFunction - }); + var url = 'http://foo.com/1'; + var server = RequestScheduler.getServerKey(url); - var promise1 = RequestScheduler.schedule(request); - var promise2 = RequestScheduler.schedule(request); - var promise3 = RequestScheduler.schedule(request); + function createRequest() { + return new Request({ + url : url, + requestFunction : requestFunction, + throttleByServer : true + }); + } + + var promise1 = RequestScheduler.request(createRequest()); + var promise2 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); + + // Scheduler is full, promise3 will be undefined + var promise3 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); - expect(deferreds.length).toBe(2); + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(2); expect(promise1).toBeDefined(); expect(promise2).toBeDefined(); expect(promise3).not.toBeDefined(); + // Scheduler now has an empty slot, promise4 goes through deferreds[0].resolve(); + RequestScheduler.update(); + + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(1); + + var promise4 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); - var promise4 = RequestScheduler.schedule(request); - expect(deferreds.length).toBe(3); + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(2); expect(promise4).toBeDefined(); - var promise5 = RequestScheduler.schedule(request); - expect(deferreds.length).toBe(3); + // Scheduler is full, promise5 will be undefined + var promise5 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); + + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(2); expect(promise5).not.toBeDefined(); + // maximumRequests increases, promise6 goes through RequestScheduler.maximumRequestsPerServer = 3; - var promise6 = RequestScheduler.schedule(request); - expect(deferreds.length).toBe(4); + var promise6 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); + + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(3); expect(promise6).toBeDefined(); - deferreds[1].resolve(); - deferreds[2].resolve(); - deferreds[3].resolve(); + var length = deferreds.length; + for (var i = 0; i < length; ++i) { + deferreds[i].resolve(); + } }); - it('getNumberOfAvailableRequests', function() { + it('honors priorityHeapLength', function() { var deferreds = []; + var requests = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } - var request = new Request({ - url : 'http://foo.com/1', - requestFunction : requestFunction - }); - - RequestScheduler.schedule(request); - RequestScheduler.schedule(request); - RequestScheduler.schedule(request); - - expect(RequestScheduler.maximumRequests).toBe(10); - expect(RequestScheduler.getNumberOfAvailableRequests()).toBe(7); - - deferreds[0].resolve(); - deferreds[1].resolve(); - deferreds[2].resolve(); + function createRequest(priority) { + var request = new Request({ + url : 'http://foo.com/1', + requestFunction : requestFunction, + throttle : true, + priority : priority + }); + requests.push(request); + return request; + } - expect(RequestScheduler.getNumberOfAvailableRequests()).toBe(10); + RequestScheduler.priorityHeapLength = 1; + var firstRequest = createRequest(0.0); + var promise = RequestScheduler.request(firstRequest); + expect(promise).toBeDefined(); + promise = RequestScheduler.request(createRequest(1.0)); + expect(promise).toBeUndefined(); + + RequestScheduler.priorityHeapLength = 3; + promise = RequestScheduler.request(createRequest(2.0)); + promise = RequestScheduler.request(createRequest(3.0)); + expect(promise).toBeDefined(); + promise = RequestScheduler.request(createRequest(4.0)); + expect(promise).toBeUndefined(); + + // A request is cancelled to accommodate the new heap length + RequestScheduler.priorityHeapLength = 2; + expect(firstRequest.state).toBe(RequestState.CANCELLED); + + var length = deferreds.length; + for (var i = 0; i < length; ++i) { + deferreds[i].resolve(); + } }); - it('getNumberOfAvailableRequestsByServer', function() { + function testImmediateRequest(url, dataOrBlobUri) { + var statistics = RequestScheduler.statistics; var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } - var requestFoo = new Request({ - url : 'http://foo.com/1', - requestFunction : requestFunction - }); - - var requestBar = new Request({ - url : 'http://bar.com/1', + var request = new Request({ + url : url, requestFunction : requestFunction }); - RequestScheduler.schedule(requestFoo); - RequestScheduler.schedule(requestFoo); - RequestScheduler.schedule(requestBar); + var promise = RequestScheduler.request(request); + expect(promise).toBeDefined(); - expect(RequestScheduler.maximumRequestsPerServer).toBe(6); - expect(RequestScheduler.getNumberOfAvailableRequestsByServer('http://foo.com')).toBe(4); - expect(RequestScheduler.getNumberOfAvailableRequestsByServer('http://bar.com')).toBe(5); + if (dataOrBlobUri) { + expect(request.serverKey).toBeUndefined(); + expect(statistics.numberOfActiveRequests).toBe(0); + } else { + expect(statistics.numberOfActiveRequests).toBe(1); + expect(RequestScheduler.numberOfActiveRequestsByServer(request.serverKey)).toBe(1); + } deferreds[0].resolve(); - deferreds[1].resolve(); - deferreds[2].resolve(); - expect(RequestScheduler.getNumberOfAvailableRequestsByServer('http://foo.com')).toBe(6); - expect(RequestScheduler.getNumberOfAvailableRequestsByServer('http://bar.com')).toBe(6); - }); + return promise.then(function() { + expect(request.state).toBe(RequestState.RECEIVED); + expect(statistics.numberOfActiveRequests).toBe(0); + if (!dataOrBlobUri) { + expect(RequestScheduler.numberOfActiveRequestsByServer(request.serverKey)).toBe(0); + } + }); + } - it('getServerName with https', function() { - var server = RequestScheduler.getServerName('https://foo.com/1'); - expect(server).toEqual('foo.com:443'); + it('data uri goes through immediately', function() { + var dataUri = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D'; + testImmediateRequest(dataUri, true); }); - it('getServerName with http', function() { - var server = RequestScheduler.getServerName('http://foo.com/1'); - expect(server).toEqual('foo.com:80'); - }); + it('blob uri goes through immediately', function() { + var uint8Array = new Uint8Array(4); + var blob = new Blob([uint8Array], { + type : 'application/octet-stream' + }); - it('getServerName throws if url is undefined', function() { - expect(function() { - return RequestScheduler.getServerName(); - }).toThrowDeveloperError(); + var blobUrl = window.URL.createObjectURL(blob); + testImmediateRequest(blobUrl, true); }); - it('hasAvailableRequests and hasAvailableRequestsByServer', function() { - RequestScheduler.maximumRequestsPerServer = 2; - RequestScheduler.maximumRequests = 3; + it('request goes through immediately when throttle is false', function() { + var url = 'https://foo.com/1'; + testImmediateRequest(url, false); + }); + it('makes a throttled request', function() { var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } - var requestFoo = new Request({ - url : 'http://foo.com/1', + var request = new Request({ + throttle : true, + url : 'https://foo.com/1', requestFunction : requestFunction }); + expect(request.state).toBe(RequestState.UNISSUED); + + var promise = RequestScheduler.request(request); + expect(promise).toBeDefined(); + expect(request.state).toBe(RequestState.ISSUED); + + RequestScheduler.update(); + expect(request.state).toBe(RequestState.ACTIVE); - var requestBar = new Request({ - url : 'http://bar.com/1', + deferreds[0].resolve(); + expect(request.state).toBe(RequestState.RECEIVED); + }); + + it('cancels an issued request', function() { + var statistics = RequestScheduler.statistics; + + function requestFunction() { + return when.resolve(); + } + + var request = new Request({ + throttle : true, + url : 'https://foo.com/1', requestFunction : requestFunction }); - RequestScheduler.schedule(requestFoo); - expect(RequestScheduler.hasAvailableRequestsByServer('http://foo.com')).toEqual(true); - RequestScheduler.schedule(requestFoo); - expect(RequestScheduler.hasAvailableRequestsByServer('http://foo.com')).toEqual(false); + var promise = RequestScheduler.request(request); + expect(request.state).toBe(RequestState.ISSUED); - expect(RequestScheduler.hasAvailableRequests()).toEqual(true); - RequestScheduler.schedule(requestBar); - expect(RequestScheduler.hasAvailableRequests()).toEqual(false); + request.cancel(); + RequestScheduler.update(); - expect(RequestScheduler.getNumberOfAvailableRequests()).toEqual(0); + expect(request.state).toBe(RequestState.CANCELLED); + expect(statistics.numberOfCancelledRequests).toBe(1); + expect(statistics.numberOfCancelledActiveRequests).toBe(0); - deferreds[0].resolve(); - deferreds[1].resolve(); - deferreds[2].resolve(); + return promise.then(function() { + fail('should not be called'); + }).otherwise(function(error) { + expect(request.state).toBe(RequestState.CANCELLED); + }); }); - it('defers request when request scheduler is full', function() { - RequestScheduler.maximumRequests = 3; + it('cancels an active request', function() { + var statistics = RequestScheduler.statistics; + var cancelFunction = jasmine.createSpy('cancelFunction'); + function requestFunction() { + return when.defer().promise; + } + + var request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction, + cancelFunction : cancelFunction + }); + + var promise = RequestScheduler.request(request); + RequestScheduler.update(); + expect(request.state).toBe(RequestState.ACTIVE); + + request.cancel(); + RequestScheduler.update(); + + expect(request.state).toBe(RequestState.CANCELLED); + expect(statistics.numberOfCancelledRequests).toBe(1); + expect(statistics.numberOfCancelledActiveRequests).toBe(1); + expect(RequestScheduler.numberOfActiveRequestsByServer(request.serverKey)).toBe(0); + expect(cancelFunction).toHaveBeenCalled(); + + return promise.then(function() { + fail('should not be called'); + }).otherwise(function(error) { + expect(request.state).toBe(RequestState.CANCELLED); + }); + }); + + it('handles request failure', function() { + var statistics = RequestScheduler.statistics; var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } var request = new Request({ - url : 'http://foo.com/1', + url : 'https://foo.com/1', requestFunction : requestFunction }); - var requestDeferred = new Request({ - url : 'http://foo.com/1', - requestFunction : requestFunction, - defer : true + var promise = RequestScheduler.request(request); + expect(request.state).toBe(RequestState.ACTIVE); + expect(statistics.numberOfActiveRequests).toBe(1); + + deferreds[0].reject('Request failed'); + RequestScheduler.update(); + expect(statistics.numberOfActiveRequests).toBe(0); + + return promise.then(function() { + fail('should not be called'); + }).otherwise(function(error) { + expect(error).toBe('Request failed'); }); + }); - RequestScheduler.schedule(request); - RequestScheduler.schedule(request); - RequestScheduler.schedule(request); - expect(RequestScheduler.hasAvailableRequests()).toEqual(false); + it('prioritizes requests', function() { + var currentPriority = 0.0; - // A deferred request will always return a promise, however its - // requestFunction is not called until there is an open slot - var deferredPromise = RequestScheduler.schedule(requestDeferred); - expect(deferredPromise).toBeDefined(); - expect(deferreds[3]).not.toBeDefined(); + function getRequestFunction(priority) { + return function() { + expect(priority).toBeGreaterThan(currentPriority); + currentPriority = priority; + return when.resolve(); + }; + } - // When the first request completes, the deferred promise starts - deferreds[0].resolve(); - expect(deferreds[3]).toBeDefined(); + function createRequest(priority) { + return new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : getRequestFunction(priority), + priority : priority + }); + } - deferreds[1].resolve(); - deferreds[2].resolve(); - deferreds[3].resolve(); + var length = RequestScheduler.priorityHeapLength; + for (var i = 0; i < length; ++i) { + var priority = Math.random(); + RequestScheduler.request(createRequest(priority)); + } + + RequestScheduler.update(); + expect(currentPriority).toBeGreaterThan(0.0); // Ensures that the expect in getRequestFunction is actually called }); - it('makes a basic request', function() { - RequestScheduler.maximumRequests = 2; + it('updates priority', function() { + var invertPriority = false; - var deferreds = []; + function getPriorityFunction(priority) { + return function() { + if (invertPriority) { + return 1.0 - priority; + } + return priority; + }; + } - function requestFunction(url) { - var deferred = when.defer(); - deferreds.push(deferred); - return deferred.promise; + function requestFunction() { + return when.resolve(); } - var promise1 = RequestScheduler.request('http://foo.com/1', requestFunction); - var promise2 = RequestScheduler.request('http://foo.com/2', requestFunction); - var promise3 = RequestScheduler.request('http://foo.com/3', requestFunction); + function createRequest(priority) { + return new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction, + priorityFunction : getPriorityFunction(priority) + }); + } - expect(promise1).toBeDefined(); - expect(promise2).toBeDefined(); - expect(promise3).toBeDefined(); + var i; + var request; + var length = RequestScheduler.priorityHeapLength; + for (i = 0; i < length; ++i) { + var priority = i / (length - 1); + request = createRequest(priority); + request.testId = i; + RequestScheduler.request(request); + } - expect(deferreds[2]).not.toBeDefined(); + // Update priorities while not letting any requests go through + RequestScheduler.maximumRequests = 0; + RequestScheduler.update(); + + var requestHeap = RequestScheduler.requestHeap; + var requests = []; + var currentTestId = 0; + while (requestHeap.length > 0) { + request = requestHeap.pop(); + requests.push(request); + expect(request.testId).toBeGreaterThanOrEqualTo(currentTestId); + currentTestId = request.testId; + } - // When the first request completes, the last request starts - deferreds[0].resolve(); - expect(deferreds[2]).toBeDefined(); + for (i = 0; i < length; ++i) { + requestHeap.insert(requests[i]); + } - deferreds[1].resolve(); - deferreds[2].resolve(); + invertPriority = true; + RequestScheduler.update(); + + while (requestHeap.length > 0) { + request = requestHeap.pop(); + expect(request.testId).toBeLessThanOrEqualTo(currentTestId); + currentTestId = request.testId; + } }); - it('prioritize requests', function() { - RequestScheduler.prioritize = true; - RequestScheduler.maximumRequests = 2; + it('handles low priority requests', function() { + function requestFunction() { + return when.resolve(); + } - var deferreds = []; + function createRequest(priority) { + return new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction, + priority : priority + }); + } - function requestFunction(url) { - var deferred = when.defer(); - deferreds.push(deferred); - return deferred.promise; + var highPriority = 0.0; + var mediumPriority = 0.5; + var lowPriority = 1.0; + + var length = RequestScheduler.priorityHeapLength; + for (var i = 0; i < length; ++i) { + RequestScheduler.request(createRequest(mediumPriority)); } - var terrainRequest1 = new Request({ - url : 'http://foo.com/1', - type : RequestType.TERRAIN, - requestFunction : requestFunction, - distance : 10.0 - }); + // Heap is full so low priority request is not even issued + var promise = RequestScheduler.request(createRequest(lowPriority)); + expect(promise).toBeUndefined(); + expect(RequestScheduler.statistics.numberOfCancelledRequests).toBe(0); - var terrainRequest2 = new Request({ - url : 'http://foo.com/2', - type : RequestType.TERRAIN, - requestFunction : requestFunction, - distance : 20.0 - }); + // Heap is full so high priority request bumps off lower priority request + promise = RequestScheduler.request(createRequest(highPriority)); + expect(promise).toBeDefined(); + expect(RequestScheduler.statistics.numberOfCancelledRequests).toBe(1); + }); - var imageryRequest = new Request({ - url : 'http://bar.com/1', - type : RequestType.IMAGERY, - requestFunction : requestFunction, - distance : 15.0 - }); + it('unthrottled requests starve throttled requests', function() { + var deferreds = []; - var promise1 = RequestScheduler.schedule(terrainRequest1); - var promise2 = RequestScheduler.schedule(terrainRequest2); - var promise3 = RequestScheduler.schedule(imageryRequest); + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } - // The requests should all return undefined because the budgets haven't been created yet - expect(promise1).not.toBeDefined(); - expect(promise2).not.toBeDefined(); - expect(promise3).not.toBeDefined(); + function createRequest(throttle) { + return new Request({ + url : 'http://foo.com/1', + requestFunction : requestFunction, + throttle : throttle + }); + } - // Budgets should now allow one terrain request and one imagery request (based on their distances) - RequestScheduler.resetBudgets(); + var throttledRequest = createRequest(true); + RequestScheduler.request(throttledRequest); - promise1 = RequestScheduler.schedule(terrainRequest1); - promise2 = RequestScheduler.schedule(terrainRequest2); - promise3 = RequestScheduler.schedule(imageryRequest); + for (var i = 0; i < RequestScheduler.maximumRequests; ++i) { + RequestScheduler.request(createRequest(false)); + } + RequestScheduler.update(); - expect(promise1).toBeDefined(); - expect(promise2).not.toBeDefined(); - expect(promise3).toBeDefined(); + expect(throttledRequest.state).toBe(RequestState.ISSUED); + // Resolve one of the unthrottled requests deferreds[0].resolve(); - deferreds[1].resolve(); + RequestScheduler.update(); + expect(throttledRequest.state).toBe(RequestState.ACTIVE); - RequestScheduler.resetBudgets(); - RequestScheduler.prioritize = false; + var length = deferreds.length; + for (var j = 0; j < length; ++j) { + deferreds[j].resolve(); + } }); - it('does not throttle requests when RequestScheduler.throttle is false', function() { - RequestScheduler.throttle = false; - RequestScheduler.maximumRequests = 2; - + it('request throttled by server is cancelled', function() { var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } + function createRequest(throttleByServer) { + return new Request({ + url : 'http://foo.com/1', + requestFunction : requestFunction, + throttleByServer : throttleByServer + }); + } + + for (var i = 0; i < RequestScheduler.maximumRequestsPerServer - 1; ++i) { + RequestScheduler.request(createRequest(false)); + } + + var throttledRequest = createRequest(true); + RequestScheduler.request(throttledRequest); + RequestScheduler.request(createRequest(false)); + + RequestScheduler.update(); + expect(throttledRequest.state).toBe(RequestState.CANCELLED); + + var length = deferreds.length; + for (var j = 0; j < length; ++j) { + deferreds[j].resolve(); + } + }); + + it('throttleRequests', function() { + RequestScheduler.maximumRequests = 0; + + function requestFunction() { + return when.resolve(); + } + + RequestScheduler.throttleRequests = true; var request = new Request({ - url : 'http://foo.com/', + throttle : true, + url : 'https://foo.com/1', requestFunction : requestFunction }); + var promise = RequestScheduler.request(request); + expect(promise).toBeUndefined(); - var promise1 = RequestScheduler.schedule(request); - var promise2 = RequestScheduler.schedule(request); - var promise3 = RequestScheduler.schedule(request); - - // All requests are passed through to the browser - expect(promise1).toBeDefined(); - expect(promise2).toBeDefined(); - expect(promise3).toBeDefined(); - - deferreds[0].resolve(); - deferreds[1].resolve(); - deferreds[2].resolve(); + RequestScheduler.throttleRequests = false; + request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + promise = RequestScheduler.request(request); + expect(promise).toBeDefined(); - RequestScheduler.throttle = true; + RequestScheduler.throttleRequests = true; }); it('debugShowStatistics', function() { spyOn(console, 'log'); + RequestScheduler.debugShowStatistics = true; var deferreds = []; - function requestFunction(url) { + function requestFunction() { var deferred = when.defer(); deferreds.push(deferred); return deferred.promise; } - var request = new Request({ - url : 'http://foo.com/1', - requestFunction : requestFunction - }); + function createRequest() { + return new Request({ + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + } - RequestScheduler.debugShowStatistics = false; - RequestScheduler.schedule(request); - RequestScheduler.resetBudgets(); - expect(console.log).not.toHaveBeenCalled(); + var requestToCancel = createRequest(); + RequestScheduler.request(createRequest()); + RequestScheduler.request(createRequest()); + RequestScheduler.request(requestToCancel); + RequestScheduler.update(); - RequestScheduler.debugShowStatistics = true; - RequestScheduler.schedule(request); - RequestScheduler.resetBudgets(); - expect(console.log).toHaveBeenCalled(); + expect(console.log).toHaveBeenCalledWith('Number of attempted requests: 3'); + expect(console.log).toHaveBeenCalledWith('Number of active requests: 3'); - deferreds[0].resolve(); - deferreds[1].resolve(); + deferreds[0].reject(); + requestToCancel.cancel(); + RequestScheduler.update(); + + expect(console.log).toHaveBeenCalledWith('Number of cancelled requests: 1'); + expect(console.log).toHaveBeenCalledWith('Number of cancelled active requests: 1'); + expect(console.log).toHaveBeenCalledWith('Number of failed requests: 1'); + + var length = deferreds.length; + for (var i = 0; i < length; ++i) { + deferreds[i].resolve(); + } RequestScheduler.debugShowStatistics = false; - RequestScheduler.resetBudgets(); }); }); diff --git a/Specs/Core/VRTheWorldTerrainProviderSpec.js b/Specs/Core/VRTheWorldTerrainProviderSpec.js index f04ccee3a01..cf5858abc29 100644 --- a/Specs/Core/VRTheWorldTerrainProviderSpec.js +++ b/Specs/Core/VRTheWorldTerrainProviderSpec.js @@ -7,6 +7,8 @@ defineSuite([ 'Core/loadImage', 'Core/loadWithXhr', 'Core/Math', + 'Core/Request', + 'Core/RequestScheduler', 'Core/TerrainProvider', 'Specs/pollToPromise', 'ThirdParty/when' @@ -18,12 +20,15 @@ defineSuite([ loadImage, loadWithXhr, CesiumMath, + Request, + RequestScheduler, TerrainProvider, pollToPromise, when) { 'use strict'; beforeEach(function() { + RequestScheduler.clearForSpecs(); loadWithXhr.load = function(url, responseType, method, data, headers, deferred, overrideMimeType) { setTimeout(function() { var parser = new DOMParser(); @@ -58,6 +63,12 @@ defineSuite([ loadWithXhr.load = loadWithXhr.defaultLoad; }); + function createRequest() { + return new Request({ + throttleByServer : true + }); + } + it('conforms to TerrainProvider interface', function() { expect(VRTheWorldTerrainProvider).toConformToInterface(TerrainProvider); }); @@ -273,15 +284,15 @@ defineSuite([ return pollToPromise(function() { return terrainProvider.ready; }).then(function() { - var promise = terrainProvider.requestTileGeometry(0, 0, 0); - expect(promise).toBeDefined(); - + var promise; var i; - for (i = 0; i < 10; ++i) { - promise = terrainProvider.requestTileGeometry(0, 0, 0); + for (i = 0; i < RequestScheduler.maximumRequestsPerServer; ++i) { + promise = terrainProvider.requestTileGeometry(0, 0, 0, createRequest()); } + RequestScheduler.update(); + expect(promise).toBeDefined(); - promise = terrainProvider.requestTileGeometry(0, 0, 0); + promise = terrainProvider.requestTileGeometry(0, 0, 0, createRequest()); expect(promise).toBeUndefined(); for (i = 0; i < deferreds.length; ++i) { diff --git a/Specs/Core/isBlobUriSpec.js b/Specs/Core/isBlobUriSpec.js new file mode 100644 index 00000000000..d319b0a7bf5 --- /dev/null +++ b/Specs/Core/isBlobUriSpec.js @@ -0,0 +1,27 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/isBlobUri' + ], function( + isBlobUri) { + 'use strict'; + + it('Throws if url is undefined', function() { + expect(function() { + isBlobUri(undefined); + }).toThrowDeveloperError(); + }); + + it('Determines that a uri is not a blob uri', function() { + expect(isBlobUri('http://cesiumjs.org/')).toEqual(false); + }); + + it('Determines that a uri is a blob uri', function() { + var uint8Array = new Uint8Array(4); + var blob = new Blob([uint8Array], { + type : 'application/octet-stream' + }); + + var blobUrl = window.URL.createObjectURL(blob); + expect(isBlobUri(blobUrl)).toEqual(true); + }); +}); diff --git a/Specs/Core/isDataUriSpec.js b/Specs/Core/isDataUriSpec.js index 9f2b4294de1..b3864848630 100644 --- a/Specs/Core/isDataUriSpec.js +++ b/Specs/Core/isDataUriSpec.js @@ -5,8 +5,13 @@ defineSuite([ isDataUri) { 'use strict'; + it('Throws if url is undefined', function() { + expect(function() { + isDataUri(undefined); + }).toThrowDeveloperError(); + }); + it('Determines that a uri is not a data uri', function() { - expect(isDataUri(undefined)).toEqual(false); expect(isDataUri('http://cesiumjs.org/')).toEqual(false); }); diff --git a/Specs/Core/loadArrayBufferSpec.js b/Specs/Core/loadArrayBufferSpec.js index b6c6ad825d7..11b5bdd5407 100644 --- a/Specs/Core/loadArrayBufferSpec.js +++ b/Specs/Core/loadArrayBufferSpec.js @@ -1,10 +1,14 @@ /*global defineSuite*/ defineSuite([ 'Core/loadArrayBuffer', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadArrayBuffer, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; var fakeXHR; @@ -135,4 +139,19 @@ defineSuite([ expect(rejectedError.statusCode).toEqual(404); expect(rejectedError.response).toEqual(error); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadArrayBuffer(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadBlobSpec.js b/Specs/Core/loadBlobSpec.js index 449fd2edc6a..af7f4059988 100644 --- a/Specs/Core/loadBlobSpec.js +++ b/Specs/Core/loadBlobSpec.js @@ -1,10 +1,14 @@ /*global defineSuite*/ defineSuite([ 'Core/loadBlob', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadBlob, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; var fakeXHR; @@ -135,4 +139,19 @@ defineSuite([ expect(rejectedError.statusCode).toEqual(404); expect(rejectedError.response).toEqual(error); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadBlob(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadCRNSpec.js b/Specs/Core/loadCRNSpec.js index 7377bb260ba..c184d0ab71e 100644 --- a/Specs/Core/loadCRNSpec.js +++ b/Specs/Core/loadCRNSpec.js @@ -2,11 +2,15 @@ defineSuite([ 'Core/loadCRN', 'Core/PixelFormat', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadCRN, PixelFormat, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; var validCompressed = new Uint8Array([72, 120, 0, 74, 227, 123, 0, 0, 0, 138, 92, 167, 0, 4, 0, 4, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 22, 0, 1, 0, 0, 96, 0, 0, 12, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 0, 0, 108, 0, 0, 0, 137, 0, 10, 96, 0, 0, 0, 0, 0, 0, 16, 4, 9, 130, 0, 0, 0, 0, 0, 0, 109, 4, 0, 0, 198, 96, 128, 0, 0, 0, 0, 0, 26, 80, 0, 0, 6, 96, 0, 0, 0, 0, 0, 0, 16, 0, 51, 0, 0, 0, 0, 0, 0, 0, 128, 1, 152, 0, 0, 0, 0, 0, 0, 4, 0]); @@ -145,4 +149,19 @@ defineSuite([ expect(rejectedError).toBeUndefined(); }); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadCRN(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadImageSpec.js b/Specs/Core/loadImageSpec.js index 2a31f4cb2bd..9837b4e7941 100644 --- a/Specs/Core/loadImageSpec.js +++ b/Specs/Core/loadImageSpec.js @@ -1,9 +1,13 @@ /*global defineSuite*/ defineSuite([ 'Core/loadImage', + 'Core/Request', + 'Core/RequestScheduler', 'ThirdParty/when' ], function( loadImage, + Request, + RequestScheduler, when) { 'use strict'; @@ -114,4 +118,19 @@ defineSuite([ expect(failure).toEqual(true); expect(loadedImage).toBeUndefined(); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadImage(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadImageViaBlobSpec.js b/Specs/Core/loadImageViaBlobSpec.js index bfbe82e5b96..440a4b6c574 100644 --- a/Specs/Core/loadImageViaBlobSpec.js +++ b/Specs/Core/loadImageViaBlobSpec.js @@ -1,9 +1,13 @@ /*global defineSuite*/ defineSuite([ 'Core/loadImageViaBlob', + 'Core/Request', + 'Core/RequestScheduler', 'ThirdParty/when' ], function( loadImageViaBlob, + Request, + RequestScheduler, when) { 'use strict'; @@ -78,4 +82,19 @@ defineSuite([ expect(failure).toEqual(true); expect(loadedImage).toBeUndefined(); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadImageViaBlob(testUrl, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadJsonSpec.js b/Specs/Core/loadJsonSpec.js index 123763021d5..ac2618220a3 100644 --- a/Specs/Core/loadJsonSpec.js +++ b/Specs/Core/loadJsonSpec.js @@ -1,10 +1,14 @@ /*global defineSuite*/ defineSuite([ 'Core/loadJson', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadJson, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; var fakeXHR; @@ -139,4 +143,19 @@ defineSuite([ expect(rejectedError.statusCode).toEqual(404); expect(rejectedError.response).toEqual(error); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadJson(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadJsonpSpec.js b/Specs/Core/loadJsonpSpec.js index 9b735b69217..7cc442fe470 100644 --- a/Specs/Core/loadJsonpSpec.js +++ b/Specs/Core/loadJsonpSpec.js @@ -1,10 +1,14 @@ /*global defineSuite*/ defineSuite([ 'Core/loadJsonp', - 'Core/DefaultProxy' + 'Core/DefaultProxy', + 'Core/Request', + 'Core/RequestScheduler' ], function( loadJsonp, - DefaultProxy) { + DefaultProxy, + Request, + RequestScheduler) { 'use strict'; it('throws with no url', function() { @@ -66,4 +70,19 @@ defineSuite([ }); loadJsonp(testUrl, options); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadJsonp(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadKTXSpec.js b/Specs/Core/loadKTXSpec.js index 3718ca996ca..387854c68e7 100644 --- a/Specs/Core/loadKTXSpec.js +++ b/Specs/Core/loadKTXSpec.js @@ -2,12 +2,16 @@ defineSuite([ 'Core/loadKTX', 'Core/PixelFormat', + 'Core/Request', 'Core/RequestErrorEvent', + 'Core/RequestScheduler', 'Core/RuntimeError' ], function( loadKTX, PixelFormat, + Request, RequestErrorEvent, + RequestScheduler, RuntimeError) { 'use strict'; @@ -333,7 +337,7 @@ defineSuite([ expect(rejectedError.message).toEqual('3D textures are unsupported.'); }); - it('Texture arrays are unsupported', function() { + it('texture arrays are unsupported', function() { var reinterprestBuffer = new Uint32Array(validUncompressed.buffer); var invalidKTX = new Uint32Array(reinterprestBuffer); invalidKTX[12] = 15; @@ -353,7 +357,7 @@ defineSuite([ expect(rejectedError.message).toEqual('Texture arrays are unsupported.'); }); - it('Cubemaps are unsupported', function() { + it('cubemaps are unsupported', function() { var reinterprestBuffer = new Uint32Array(validUncompressed.buffer); var invalidKTX = new Uint32Array(reinterprestBuffer); invalidKTX[13] = 6; @@ -372,4 +376,19 @@ defineSuite([ expect(rejectedError instanceof RuntimeError).toEqual(true); expect(rejectedError.message).toEqual('Cubemaps are unsupported.'); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadKTX(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadTextSpec.js b/Specs/Core/loadTextSpec.js index 74c417d48a7..62bc5620170 100644 --- a/Specs/Core/loadTextSpec.js +++ b/Specs/Core/loadTextSpec.js @@ -1,10 +1,14 @@ /*global defineSuite*/ defineSuite([ 'Core/loadText', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadText, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; var fakeXHR; @@ -135,4 +139,19 @@ defineSuite([ expect(rejectedError.statusCode).toEqual(404); expect(rejectedError.response).toEqual(error); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadText(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/Core/loadWithXhrSpec.js b/Specs/Core/loadWithXhrSpec.js index 6b2b5a5742e..84aa74f13f0 100644 --- a/Specs/Core/loadWithXhrSpec.js +++ b/Specs/Core/loadWithXhrSpec.js @@ -2,11 +2,15 @@ defineSuite([ 'Core/loadWithXhr', 'Core/loadImage', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadWithXhr, loadImage, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; it('throws with no url', function() { @@ -15,6 +19,24 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadWithXhr({ + url : testUrl, + request : request + }); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); + describe('data URI loading', function() { it('can load URI escaped text with default response type', function() { return loadWithXhr({ diff --git a/Specs/Core/loadXMLSpec.js b/Specs/Core/loadXMLSpec.js index 8c023a8df2b..b79a58909b9 100644 --- a/Specs/Core/loadXMLSpec.js +++ b/Specs/Core/loadXMLSpec.js @@ -1,10 +1,14 @@ /*global defineSuite*/ defineSuite([ 'Core/loadXML', - 'Core/RequestErrorEvent' + 'Core/Request', + 'Core/RequestErrorEvent', + 'Core/RequestScheduler' ], function( loadXML, - RequestErrorEvent) { + Request, + RequestErrorEvent, + RequestScheduler) { 'use strict'; var fakeXHR; @@ -137,4 +141,19 @@ defineSuite([ expect(rejectedError.statusCode).toEqual(404); expect(rejectedError.response).toEqual(error); }); + + it('returns undefined if the request is throttled', function() { + var oldMaximumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; + + var request = new Request({ + throttle : true + }); + + var testUrl = 'http://example.invalid/testuri'; + var promise = loadXML(testUrl, undefined, request); + expect(promise).toBeUndefined(); + + RequestScheduler.maximumRequests = oldMaximumRequests; + }); }); diff --git a/Specs/DomEventSimulator.js b/Specs/DomEventSimulator.js index 75c99d375db..bf84b47fdb3 100644 --- a/Specs/DomEventSimulator.js +++ b/Specs/DomEventSimulator.js @@ -215,8 +215,18 @@ define([ var gamma = defaultValue(options.gamma, 0.0); var absolute = defaultValue(options.absolute, false); - var event = document.createEvent('DeviceOrientationEvent'); - event.initDeviceOrientationEvent(type, canBubble, cancelable, alpha, beta, gamma, absolute); + var event; + event = document.createEvent('DeviceOrientationEvent'); + if (typeof event.initDeviceOrientationEvent === 'function') { + event.initDeviceOrientationEvent(type, canBubble, cancelable, alpha, beta, gamma, absolute); + } else { + event = new DeviceOrientationEvent('deviceorientation', { + alpha : alpha, + beta : beta, + gamma : gamma, + absolute : absolute + }); + } return event; } diff --git a/Specs/Scene/ArcGisMapServerImageryProviderSpec.js b/Specs/Scene/ArcGisMapServerImageryProviderSpec.js index 14276f52a1f..1dcde473ebb 100644 --- a/Specs/Scene/ArcGisMapServerImageryProviderSpec.js +++ b/Specs/Scene/ArcGisMapServerImageryProviderSpec.js @@ -12,6 +12,7 @@ defineSuite([ 'Core/loadWithXhr', 'Core/queryToObject', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorProjection', 'Core/WebMercatorTilingScheme', 'Scene/DiscardMissingTileImagePolicy', @@ -35,6 +36,7 @@ defineSuite([ loadWithXhr, queryToObject, Rectangle, + RequestScheduler, WebMercatorProjection, WebMercatorTilingScheme, DiscardMissingTileImagePolicy, @@ -47,6 +49,10 @@ defineSuite([ Uri) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadJsonp.loadAndExecuteScript = loadJsonp.defaultLoadAndExecuteScript; loadImage.createImage = loadImage.defaultCreateImage; @@ -602,6 +608,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -622,6 +631,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/BingMapsImageryProviderSpec.js b/Specs/Scene/BingMapsImageryProviderSpec.js index ca2f087ef34..192a04c2374 100644 --- a/Specs/Scene/BingMapsImageryProviderSpec.js +++ b/Specs/Scene/BingMapsImageryProviderSpec.js @@ -6,6 +6,7 @@ defineSuite([ 'Core/loadImage', 'Core/loadJsonp', 'Core/loadWithXhr', + 'Core/RequestScheduler', 'Core/WebMercatorTilingScheme', 'Scene/BingMapsStyle', 'Scene/DiscardMissingTileImagePolicy', @@ -21,6 +22,7 @@ defineSuite([ loadImage, loadJsonp, loadWithXhr, + RequestScheduler, WebMercatorTilingScheme, BingMapsStyle, DiscardMissingTileImagePolicy, @@ -31,6 +33,10 @@ defineSuite([ pollToPromise) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadJsonp.loadAndExecuteScript = loadJsonp.defaultLoadAndExecuteScript; loadImage.createImage = loadImage.defaultCreateImage; @@ -364,6 +370,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -399,6 +408,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/CameraSpec.js b/Specs/Scene/CameraSpec.js index 21403eb8147..63e083a0e03 100644 --- a/Specs/Scene/CameraSpec.js +++ b/Specs/Scene/CameraSpec.js @@ -1223,6 +1223,25 @@ defineSuite([ expect(camera.frustum.bottom).toEqual(-1.25, CesiumMath.EPSILON10); }); + it('zooms out 2D when frustrum has greater height than width', function() { + var frustum = new OrthographicOffCenterFrustum(); + frustum.near = 1.0; + frustum.far = 2.0; + frustum.left = -1.0; + frustum.right = 1.0; + frustum.top = 2.0; + frustum.bottom = -2.0; + camera.frustum = frustum; + + camera.update(SceneMode.SCENE2D); + + camera.zoomOut(zoomAmount); + expect(camera.frustum.right).toEqualEpsilon(1.25, CesiumMath.EPSILON10); + expect(camera.frustum.left).toEqual(-1.25, CesiumMath.EPSILON10); + expect(camera.frustum.top).toEqual(2.5, CesiumMath.EPSILON10); + expect(camera.frustum.bottom).toEqual(-2.5, CesiumMath.EPSILON10); + }); + it('zooms in 2D', function() { var frustum = new OrthographicOffCenterFrustum(); frustum.near = 1.0; @@ -1242,6 +1261,25 @@ defineSuite([ expect(camera.frustum.bottom).toEqual(-0.75, CesiumMath.EPSILON10); }); + it('zooms in 2D when frustrum has greater height than width', function() { + var frustum = new OrthographicOffCenterFrustum(); + frustum.near = 1.0; + frustum.far = 2.0; + frustum.left = -1.0; + frustum.right = 1.0; + frustum.top = 2.0; + frustum.bottom = -2.0; + camera.frustum = frustum; + + camera.update(SceneMode.SCENE2D); + + camera.zoomIn(zoomAmount); + expect(camera.frustum.right).toEqualEpsilon(0.75, CesiumMath.EPSILON10); + expect(camera.frustum.left).toEqual(-0.75, CesiumMath.EPSILON10); + expect(camera.frustum.top).toEqual(1.5, CesiumMath.EPSILON10); + expect(camera.frustum.bottom).toEqual(-1.5, CesiumMath.EPSILON10); + }); + it('clamps zoom out in 2D', function() { var frustum = new OrthographicOffCenterFrustum(); frustum.near = 1.0; @@ -2850,4 +2888,12 @@ defineSuite([ camera.switchToPerspectiveFrustum(); expect(camera.frustum instanceof OrthographicOffCenterFrustum).toEqual(true); }); + + it('normalizes WC members', function() { + var transform = Matrix4.fromScale(new Cartesian3(2, 2, 2)); + camera.lookAtTransform(transform); + expect(Cartesian3.magnitude(camera.directionWC)).toEqualEpsilon(1.0, CesiumMath.EPSILON15); + expect(Cartesian3.magnitude(camera.rightWC)).toEqualEpsilon(1.0, CesiumMath.EPSILON15); + expect(Cartesian3.magnitude(camera.upWC)).toEqualEpsilon(1.0, CesiumMath.EPSILON15); + }); }); diff --git a/Specs/Scene/GlobeSurfaceTileSpec.js b/Specs/Scene/GlobeSurfaceTileSpec.js index a0fd47f861f..4268b3f602e 100644 --- a/Specs/Scene/GlobeSurfaceTileSpec.js +++ b/Specs/Scene/GlobeSurfaceTileSpec.js @@ -9,6 +9,7 @@ defineSuite([ 'Core/GeographicTilingScheme', 'Core/Ray', 'Core/Rectangle', + 'Core/RequestScheduler', 'Scene/Imagery', 'Scene/ImageryLayer', 'Scene/ImageryLayerCollection', @@ -30,6 +31,7 @@ defineSuite([ GeographicTilingScheme, Ray, Rectangle, + RequestScheduler, Imagery, ImageryLayer, ImageryLayerCollection, @@ -143,6 +145,7 @@ defineSuite([ it('once a root tile is loaded, its children get both loadedTerrain and upsampledTerrain', function() { return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return rootTile.state === QuadtreeTileLoadState.DONE; }).then(function() { var children = rootTile.children; @@ -157,6 +160,7 @@ defineSuite([ it('loaded terrainData is copied to the tile once it is available', function() { return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return rootTile.data.loadedTerrain.state >= TerrainState.RECEIVED; }).then(function() { expect(rootTile.data.terrainData).toBeDefined(); @@ -281,6 +285,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return rootTile.renderable && childTile.renderable; }).then(function() { expect(childTile.data.waterMaskTexture).toBeDefined(); @@ -290,6 +295,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToDestroy); + RequestScheduler.update(); return childTile.state === QuadtreeTileLoadState.DONE; }).then(function() { expect(childTile.data.waterMaskTexture).toBeDefined(); @@ -306,6 +312,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysFailTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return rootTile.renderable && childTile.renderable; }).then(function() { expect(childTile.data.loadedTerrain).toBeUndefined(); @@ -320,6 +327,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return defined(rootTile.data.terrainData) && defined(rootTile.data.terrainData._mesh) && defined(childTile.data.terrainData); }).then(function() { @@ -328,6 +336,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return grandchildTile.state === QuadtreeTileLoadState.DONE; }).then(function() { expect(grandchildTile.data.loadedTerrain).toBeUndefined(); @@ -337,6 +346,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToDestroy); + RequestScheduler.update(); return childTile.state === QuadtreeTileLoadState.DONE; }).then(function() { expect(grandchildTile.state).toBe(QuadtreeTileLoadState.DONE); @@ -357,6 +367,7 @@ defineSuite([ GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return defined(rootTile.data.terrainData) && defined(rootTile.data.terrainData._mesh) && defined(childTile.data.terrainData) && defined(childTile.data.terrainData._mesh) && defined(grandchildTile.data.terrainData); @@ -366,6 +377,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(greatGrandchildTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return greatGrandchildTile.state === QuadtreeTileLoadState.DONE; }).then(function() { expect(greatGrandchildTile.data.loadedTerrain).toBeUndefined(); @@ -376,6 +388,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, realTerrainProvider, imageryLayerCollection, vertexArraysToBeDestroyed); GlobeSurfaceTile.processStateMachine(grandchildTile, scene.frameState, alwaysDeferTerrainProvider, imageryLayerCollection, vertexArraysToBeDestroyed); + RequestScheduler.update(); return childTile.state === QuadtreeTileLoadState.DONE && !defined(grandchildTile.data.upsampledTerrain); }).then(function() { @@ -394,6 +407,7 @@ defineSuite([ return pollToPromise(function() { GlobeSurfaceTile.processStateMachine(rootTile, scene.frameState, realTerrainProvider, imageryLayerCollection, []); GlobeSurfaceTile.processStateMachine(childTile, scene.frameState, alwaysFailTerrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return rootTile.state >= QuadtreeTileLoadState.DONE && childTile.state >= QuadtreeTileLoadState.DONE; }).then(function() { @@ -538,6 +552,7 @@ defineSuite([ } GlobeSurfaceTile.processStateMachine(tile, scene.frameState, terrainProvider, imageryLayerCollection, []); + RequestScheduler.update(); return tile.state === QuadtreeTileLoadState.DONE; }).then(function() { var ray = new Ray( diff --git a/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js b/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js index 0e23da6755b..00eec62d9ac 100644 --- a/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js +++ b/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js @@ -11,6 +11,7 @@ defineSuite([ 'Core/loadImage', 'Core/loadWithXhr', 'Core/Rectangle', + 'Core/RequestScheduler', 'Scene/DiscardMissingTileImagePolicy', 'Scene/Imagery', 'Scene/ImageryLayer', @@ -30,6 +31,7 @@ defineSuite([ loadImage, loadWithXhr, Rectangle, + RequestScheduler, DiscardMissingTileImagePolicy, Imagery, ImageryLayer, @@ -39,6 +41,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + beforeAll(function() { decodeGoogleEarthEnterpriseData.passThroughDataForTesting = true; }); @@ -259,6 +265,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadWithXhr.load = function(url, responseType, method, data, headers, deferred, overrideMimeType) { @@ -279,6 +288,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js b/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js index 5fec67730cf..7fa6649ef47 100644 --- a/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js +++ b/Specs/Scene/GoogleEarthEnterpriseMapsProviderSpec.js @@ -6,6 +6,7 @@ defineSuite([ 'Core/loadImage', 'Core/loadWithXhr', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorTilingScheme', 'Scene/Imagery', 'Scene/ImageryLayer', @@ -19,6 +20,7 @@ defineSuite([ loadImage, loadWithXhr, Rectangle, + RequestScheduler, WebMercatorTilingScheme, Imagery, ImageryLayer, @@ -307,6 +309,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -342,6 +347,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/ImageryLayerSpec.js b/Specs/Scene/ImageryLayerSpec.js index 4c9ec1fcbcb..151b91d5d0c 100644 --- a/Specs/Scene/ImageryLayerSpec.js +++ b/Specs/Scene/ImageryLayerSpec.js @@ -6,6 +6,7 @@ defineSuite([ 'Core/loadJsonp', 'Core/loadWithXhr', 'Core/Rectangle', + 'Core/RequestScheduler', 'Renderer/ComputeEngine', 'Scene/ArcGisMapServerImageryProvider', 'Scene/BingMapsImageryProvider', @@ -28,6 +29,7 @@ defineSuite([ loadJsonp, loadWithXhr, Rectangle, + RequestScheduler, ComputeEngine, ArcGisMapServerImageryProvider, BingMapsImageryProvider, @@ -104,6 +106,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; @@ -166,6 +169,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; @@ -202,6 +206,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 1, 3); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; @@ -237,6 +242,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 1, 3); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; @@ -292,6 +298,7 @@ defineSuite([ var imagery = new Imagery(layer, 4400, 2686, 13); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; @@ -327,6 +334,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; @@ -386,6 +394,7 @@ defineSuite([ return provider.ready; }).then(function() { imageryLayer._requestImagery(new Imagery(imageryLayer, 0, 0, 0)); + RequestScheduler.update(); return pollToPromise(function() { return errorRaised; diff --git a/Specs/Scene/MapboxImageryProviderSpec.js b/Specs/Scene/MapboxImageryProviderSpec.js index 60e382b7b09..50b681bd441 100644 --- a/Specs/Scene/MapboxImageryProviderSpec.js +++ b/Specs/Scene/MapboxImageryProviderSpec.js @@ -5,6 +5,7 @@ defineSuite([ 'Core/loadImage', 'Core/Math', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorTilingScheme', 'Scene/Imagery', 'Scene/ImageryLayer', @@ -17,6 +18,7 @@ defineSuite([ loadImage, CesiumMath, Rectangle, + RequestScheduler, WebMercatorTilingScheme, Imagery, ImageryLayer, @@ -25,6 +27,10 @@ defineSuite([ pollToPromise) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); @@ -250,6 +256,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -270,6 +279,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/UrlTemplateImageryProviderSpec.js b/Specs/Scene/UrlTemplateImageryProviderSpec.js index 41b3af874c2..f3fbe8d77e9 100644 --- a/Specs/Scene/UrlTemplateImageryProviderSpec.js +++ b/Specs/Scene/UrlTemplateImageryProviderSpec.js @@ -7,6 +7,7 @@ defineSuite([ 'Core/loadImage', 'Core/Math', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorProjection', 'Core/WebMercatorTilingScheme', 'Scene/GetFeatureInfoFormat', @@ -24,6 +25,7 @@ defineSuite([ loadImage, CesiumMath, Rectangle, + RequestScheduler, WebMercatorProjection, WebMercatorTilingScheme, GetFeatureInfoFormat, @@ -35,6 +37,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); @@ -204,6 +210,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -224,6 +233,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/WebMapServiceImageryProviderSpec.js b/Specs/Scene/WebMapServiceImageryProviderSpec.js index e97d27bbe36..3f729fd6469 100644 --- a/Specs/Scene/WebMapServiceImageryProviderSpec.js +++ b/Specs/Scene/WebMapServiceImageryProviderSpec.js @@ -10,6 +10,7 @@ defineSuite([ 'Core/Math', 'Core/queryToObject', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorTilingScheme', 'Scene/GetFeatureInfoFormat', 'Scene/Imagery', @@ -30,6 +31,7 @@ defineSuite([ CesiumMath, queryToObject, Rectangle, + RequestScheduler, WebMercatorTilingScheme, GetFeatureInfoFormat, Imagery, @@ -41,6 +43,10 @@ defineSuite([ Uri) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; loadWithXhr.load = loadWithXhr.defaultLoad; @@ -703,6 +709,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -720,6 +729,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/WebMapTileServiceImageryProviderSpec.js b/Specs/Scene/WebMapTileServiceImageryProviderSpec.js index 078a18542a6..11398eba7f4 100644 --- a/Specs/Scene/WebMapTileServiceImageryProviderSpec.js +++ b/Specs/Scene/WebMapTileServiceImageryProviderSpec.js @@ -6,6 +6,7 @@ defineSuite([ 'Core/GeographicTilingScheme', 'Core/loadImage', 'Core/queryToObject', + 'Core/RequestScheduler', 'Core/WebMercatorTilingScheme', 'Scene/Imagery', 'Scene/ImageryLayer', @@ -20,6 +21,7 @@ defineSuite([ GeographicTilingScheme, loadImage, queryToObject, + RequestScheduler, WebMercatorTilingScheme, Imagery, ImageryLayer, @@ -29,6 +31,10 @@ defineSuite([ Uri) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); @@ -361,6 +367,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -381,6 +390,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/createOpenStreetMapImageryProviderSpec.js b/Specs/Scene/createOpenStreetMapImageryProviderSpec.js index 77800b4d298..041b175ca4c 100644 --- a/Specs/Scene/createOpenStreetMapImageryProviderSpec.js +++ b/Specs/Scene/createOpenStreetMapImageryProviderSpec.js @@ -5,6 +5,7 @@ defineSuite([ 'Core/loadImage', 'Core/Math', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorTilingScheme', 'Scene/Imagery', 'Scene/ImageryLayer', @@ -17,6 +18,7 @@ defineSuite([ loadImage, CesiumMath, Rectangle, + RequestScheduler, WebMercatorTilingScheme, Imagery, ImageryLayer, @@ -25,6 +27,10 @@ defineSuite([ pollToPromise) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); @@ -223,6 +229,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -243,6 +252,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Specs/Scene/createTileMapServiceImageryProviderSpec.js b/Specs/Scene/createTileMapServiceImageryProviderSpec.js index 0a804a78138..fb1cfc52369 100644 --- a/Specs/Scene/createTileMapServiceImageryProviderSpec.js +++ b/Specs/Scene/createTileMapServiceImageryProviderSpec.js @@ -11,6 +11,7 @@ defineSuite([ 'Core/loadWithXhr', 'Core/Math', 'Core/Rectangle', + 'Core/RequestScheduler', 'Core/WebMercatorProjection', 'Core/WebMercatorTilingScheme', 'Scene/Imagery', @@ -31,6 +32,7 @@ defineSuite([ loadWithXhr, CesiumMath, Rectangle, + RequestScheduler, WebMercatorProjection, WebMercatorTilingScheme, Imagery, @@ -41,6 +43,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; loadWithXhr.load = loadWithXhr.defaultLoad; @@ -360,6 +366,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -380,6 +389,7 @@ defineSuite([ var imagery = new Imagery(layer, 0, 0, 0); imagery.addReference(); layer._requestImagery(imagery); + RequestScheduler.update(); return pollToPromise(function() { return imagery.state === ImageryState.RECEIVED; diff --git a/Tools/eslint-config-cesium/.eslintrc.json b/Tools/eslint-config-cesium/.eslintrc.json new file mode 100644 index 00000000000..eb0e708be52 --- /dev/null +++ b/Tools/eslint-config-cesium/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "./node.js" +} diff --git a/Tools/eslint-config-cesium/CHANGES.md b/Tools/eslint-config-cesium/CHANGES.md new file mode 100644 index 00000000000..e4de4fad870 --- /dev/null +++ b/Tools/eslint-config-cesium/CHANGES.md @@ -0,0 +1,6 @@ +Change Log +========== + +### 1.0.0 - 2017-06-12 + +* Initial release. diff --git a/Tools/eslint-config-cesium/README.md b/Tools/eslint-config-cesium/README.md new file mode 100644 index 00000000000..16ded425aaa --- /dev/null +++ b/Tools/eslint-config-cesium/README.md @@ -0,0 +1,29 @@ +The official [shareable ESLint config](http://eslint.org/docs/developer-guide/shareable-configs) for the [Cesium](https://cesiumjs.org/) ecosystem. + +## Usage + +--- + +We export three ESLint configurations. + +### eslint-config-cesium + +This config contains basic Cesium syntax and style config, from which `browser` and `node` extend. Extends `eslint:recommended` with additional rules. + +### eslint-config-cesium/browser + +For use in [`AMD`](http://requirejs.org/docs/whyamd.html) modules and browser code. + +### eslint-config-cesium/node + +For use in `node` packages. + +--- + +To use any of these configs, + +1. `npm install eslint-config-cesium --save-dev` + + If using the `browser` config: `npm install eslint-plugin-html --save-dev` + +2. Add `"extends": "cesium"` to your `.eslintrc.*` files diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js new file mode 100644 index 00000000000..fc021d6350f --- /dev/null +++ b/Tools/eslint-config-cesium/browser.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + extends: './index.js', + env: { + amd: true, + browser: true + }, + plugins: [ + 'html' + ] +}; diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js new file mode 100644 index 00000000000..b3a82e9a376 --- /dev/null +++ b/Tools/eslint-config-cesium/index.js @@ -0,0 +1,37 @@ +'use strict'; + +module.exports = { + extends: 'eslint:recommended', + globals: { + DataView: false, + ArrayBuffer: false, + Float32Array: false, + Float64Array: false, + Int16Array: false, + Int32Array: false, + Int8Array: false, + Uint16Array: false, + Uint32Array: false, + Uint8Array: false, + Uint8ClampedArray: false + }, + rules: { + curly: ['error'], + eqeqeq: ['error'], + 'guard-for-in': ['error'], + 'new-cap': ['error', {properties: false}], + 'no-caller': ['error'], + 'no-console': 'off', + 'no-empty': ['error'], + 'no-extend-native': ['error'], + 'no-extra-boolean-cast': 'off', + 'no-irregular-whitespace': ['error'], + 'no-new': ['error'], + 'no-undef': ['error'], + 'no-unused-vars': ['error', {vars: 'all', args: 'all'}], + 'no-useless-escape': 'off', + semi: ['error'], + strict: ['error'], + 'wrap-iife': ['error', 'any'] + } +}; diff --git a/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js new file mode 100644 index 00000000000..2686d2251d8 --- /dev/null +++ b/Tools/eslint-config-cesium/node.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + extends: './index.js', + env: { + node: true + } +}; diff --git a/Tools/eslint-config-cesium/package.json b/Tools/eslint-config-cesium/package.json new file mode 100644 index 00000000000..afa7027fbe8 --- /dev/null +++ b/Tools/eslint-config-cesium/package.json @@ -0,0 +1,25 @@ +{ + "name": "eslint-config-cesium", + "version": "1.0.0", + "description": "ESLint shareable configs for Cesium", + "homepage": "http://cesiumjs.org", + "license": "Apache-2.0", + "author": { + "name": "Analytical Graphics, Inc.", + "url": "http://www.agi.com" + }, + "contributors": [ + { + "name": "Cesium community", + "url": "https://github.com/AnalyticalGraphicsInc/cesium/blob/master/CONTRIBUTORS.md" + } + ], + "keywords": [ + "eslint", + "cesium" + ], + "peerDependencies": { + "eslint": ">= 4", + "eslint-plugin-html": ">= 3" + } +} diff --git a/package.json b/package.json index ac2988c0a2a..9e52c4aa98f 100644 --- a/package.json +++ b/package.json @@ -39,13 +39,13 @@ "compressible": "^2.0.9", "compression": "^1.6.2", "electron": "^1.6.1", - "eslint-plugin-html": "^2.0.3", + "eslint-plugin-html": "^3.0.0", "event-stream": "^3.3.4", "express": "^4.15.0", "globby": "^6.1.0", "glsl-strip-comments": "^1.0.0", "gulp": "^3.9.1", - "gulp-eslint": "^3.0.1", + "gulp-eslint": "^4.0.0", "gulp-insert": "^0.5.0", "gulp-rename": "^1.2.2", "gulp-replace": "^0.5.4", diff --git a/server.js b/server.js index 98e30d9a624..0aee619e1c7 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,6 @@ /*eslint-env node*/ +'use strict'; (function() { - 'use strict'; - var express = require('express'); var compression = require('compression'); var url = require('url'); @@ -43,7 +42,7 @@ 'image/crn' : ['crn'], 'image/ktx' : ['ktx'], 'model/gltf+json' : ['gltf'], - 'model/gltf.binary' : ['bgltf', 'glb'], + 'model/gltf-binary' : ['bgltf', 'glb'], 'application/octet-stream' : ['b3dm', 'pnts', 'i3dm', 'cmpt'], 'text/plain' : ['glsl'] }); diff --git a/web.config b/web.config index 3091100d3d1..46951578bb0 100644 --- a/web.config +++ b/web.config @@ -17,9 +17,9 @@ - + - +