From e0d43f16f1375a196c817de533acaa1816466e89 Mon Sep 17 00:00:00 2001 From: Rachel Hwang Date: Tue, 9 May 2017 18:50:26 -0400 Subject: [PATCH 01/41] adding callback property sandcastle example --- Apps/Sandcastle/gallery/Callback Property.html | 88 ++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Apps/Sandcastle/gallery/Callback Property.html diff --git a/Apps/Sandcastle/gallery/Callback Property.html b/Apps/Sandcastle/gallery/Callback Property.html new file mode 100644 index 00000000000..d288b13b1dd --- /dev/null +++ b/Apps/Sandcastle/gallery/Callback Property.html @@ -0,0 +1,88 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + From be9e9cedb5a19ba94a7d53f55ce397bdc2c8e9e3 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 19 May 2017 16:53:18 -0400 Subject: [PATCH 02/41] New request scheduler --- CHANGES.md | 4 +- Source/Core/CesiumTerrainProvider.js | 41 ++- Source/Core/EllipsoidTerrainProvider.js | 7 +- Source/Core/GoogleEarthEnterpriseMetadata.js | 65 ++-- .../Core/GoogleEarthEnterpriseTerrainProvider.js | 43 ++- Source/Core/Heap.js | 200 +++++++++++ Source/Core/Iau2006XysData.js | 2 +- Source/Core/Request.js | 162 +++++++++ Source/Core/RequestScheduler.js | 383 +++++++++++++++++++++ Source/Core/RequestState.js | 21 ++ Source/Core/RequestType.js | 21 ++ Source/Core/TerrainProvider.js | 6 +- Source/Core/VRTheWorldTerrainProvider.js | 34 +- Source/Core/isBlobUri.js | 29 ++ Source/Core/isDataUri.js | 12 +- Source/Core/loadArrayBuffer.js | 13 +- Source/Core/loadBlob.js | 13 +- Source/Core/loadCRN.js | 15 +- Source/Core/loadImage.js | 48 ++- Source/Core/loadImageFromTypedArray.js | 15 +- Source/Core/loadImageViaBlob.js | 28 +- Source/Core/loadJson.js | 16 +- Source/Core/loadJsonp.js | 50 +-- Source/Core/loadKTX.js | 13 +- Source/Core/loadText.js | 12 +- Source/Core/loadWithXhr.js | 47 ++- Source/Core/loadXML.js | 12 +- Source/Core/sampleTerrain.js | 2 +- Source/Core/throttleRequestByServer.js | 95 ----- Source/Renderer/loadCubeMap.js | 4 +- Source/Scene/ArcGisMapServerImageryProvider.js | 5 +- Source/Scene/BingMapsImageryProvider.js | 5 +- Source/Scene/GlobeSurfaceTile.js | 23 +- .../Scene/GoogleEarthEnterpriseImageryProvider.js | 21 +- Source/Scene/GoogleEarthEnterpriseMapsProvider.js | 5 +- Source/Scene/GoogleEarthImageryProvider.js | 2 +- Source/Scene/GridImageryProvider.js | 3 +- Source/Scene/Imagery.js | 12 +- Source/Scene/ImageryLayer.js | 28 +- Source/Scene/ImageryProvider.js | 19 +- Source/Scene/MapboxImageryProvider.js | 5 +- Source/Scene/Scene.js | 3 + Source/Scene/SingleTileImageryProvider.js | 3 +- Source/Scene/TileCoordinatesImageryProvider.js | 3 +- Source/Scene/TileImagery.js | 4 +- Source/Scene/TileTerrain.js | 53 ++- Source/Scene/UrlTemplateImageryProvider.js | 10 +- Source/Scene/WebMapServiceImageryProvider.js | 5 +- Source/Scene/WebMapTileServiceImageryProvider.js | 5 +- Specs/Core/CesiumTerrainProviderSpec.js | 22 +- Specs/Core/GoogleEarthEnterpriseMetadataSpec.js | 17 +- .../GoogleEarthEnterpriseTerrainProviderSpec.js | 26 +- Specs/Core/HeapSpec.js | 119 +++++++ Specs/Core/RequestSchedulerSpec.js | 304 ++++++++++++++++ Specs/Core/VRTheWorldTerrainProviderSpec.js | 22 +- Specs/Core/isBlobUriSpec.js | 27 ++ Specs/Core/isDataUriSpec.js | 7 +- Specs/Core/loadArrayBufferSpec.js | 23 +- Specs/Core/loadBlobSpec.js | 23 +- Specs/Core/loadCRNSpec.js | 23 +- Specs/Core/loadImageSpec.js | 19 + Specs/Core/loadImageViaBlobSpec.js | 19 + Specs/Core/loadJsonSpec.js | 23 +- Specs/Core/loadJsonpSpec.js | 23 +- Specs/Core/loadKTXSpec.js | 23 +- Specs/Core/loadTextSpec.js | 23 +- Specs/Core/loadWithXhrSpec.js | 26 +- Specs/Core/loadXMLSpec.js | 23 +- Specs/Core/throttleRequestByServerSpec.js | 59 ---- Specs/Scene/ArcGisMapServerImageryProviderSpec.js | 6 + Specs/Scene/BingMapsImageryProviderSpec.js | 6 + Specs/Scene/GlobeSurfaceTileSpec.js | 15 + .../GoogleEarthEnterpriseImageryProviderSpec.js | 6 + .../Scene/GoogleEarthEnterpriseMapsProviderSpec.js | 6 + Specs/Scene/ImageryLayerSpec.js | 9 + Specs/Scene/MapboxImageryProviderSpec.js | 6 + Specs/Scene/UrlTemplateImageryProviderSpec.js | 6 + Specs/Scene/WebMapServiceImageryProviderSpec.js | 6 + .../Scene/WebMapTileServiceImageryProviderSpec.js | 6 + .../createOpenStreetMapImageryProviderSpec.js | 6 + .../createTileMapServiceImageryProviderSpec.js | 6 + Specs/customizeJasmine.js | 4 +- Specs/karma-main.js | 5 +- Specs/spec-main.js | 2 + 84 files changed, 2117 insertions(+), 456 deletions(-) create mode 100644 Source/Core/Heap.js create mode 100644 Source/Core/Request.js create mode 100644 Source/Core/RequestScheduler.js create mode 100644 Source/Core/RequestState.js create mode 100644 Source/Core/RequestType.js create mode 100644 Source/Core/isBlobUri.js delete mode 100644 Source/Core/throttleRequestByServer.js create mode 100644 Specs/Core/HeapSpec.js create mode 100644 Specs/Core/RequestSchedulerSpec.js create mode 100644 Specs/Core/isBlobUriSpec.js delete mode 100644 Specs/Core/throttleRequestByServerSpec.js diff --git a/CHANGES.md b/CHANGES.md index df45cc5bd79..36aad1f95da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,9 +2,11 @@ Change Log ========== ### 1.35 - 2017-07-05 + * 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.36, 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` is deprecated. The same applies for the `url` parameter for `loadArrayBuffer`, `loadBlob`, `loadImageViaBlob`, `loadText`, `loadJson`, `loadXML`, `loadImage`, `loadCRN`, `loadKTX`, and `loadCubeMap`. This will be removed in 1.36, instead `url` must be a string. ### 1.34 - 2017-06-01 diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index 289fa6b37e1..677b47897af 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. + * * @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.36.'); + 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/EllipsoidTerrainProvider.js b/Source/Core/EllipsoidTerrainProvider.js index c0011c62799..31189d7ec51 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. + * * @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..be0b51f72dd 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. * * @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. * * @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, + distance : request.distance + }); + 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, + distance : request.distance + }); + return populateSubtree(that, quadKey, subtreeRequest); }) .always(function() { delete subtreePromises[q]; diff --git a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js index 052d1c99af3..6ba06b43df9 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,10 @@ define([ './loadArrayBuffer', './Math', './Rectangle', + './Request', + './RequestType', './RuntimeError', './TaskProcessor', - './throttleRequestByServer', './TileProviderError' ], function( when, @@ -25,6 +27,7 @@ define([ defaultValue, defined, defineProperties, + deprecationWarning, DeveloperError, Event, GeographicTilingScheme, @@ -35,9 +38,10 @@ define([ loadArrayBuffer, CesiumMath, Rectangle, + Request, + RequestType, RuntimeError, TaskProcessor, - throttleRequestByServer, TileProviderError) { 'use strict'; @@ -339,9 +343,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. * @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 +351,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.'); @@ -439,15 +441,19 @@ define([ if (defined(terrainPromises[q])) { // Already being loaded possibly from another child, so return existing promise promise = terrainPromises[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.36.'); + request = new Request({ + throttle : request, + throttleByServer : request, + type : RequestType.TERRAIN + }); + } + + var requestPromise = loadArrayBuffer(url, undefined, request); + + if (!defined(requestPromise)) { + return undefined; // Throttled } promise = requestPromise @@ -569,7 +575,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 new file mode 100644 index 00000000000..77472566b2c --- /dev/null +++ b/Source/Core/Heap.js @@ -0,0 +1,200 @@ +/*global define*/ +define([ + './Check', + './defaultValue', + './defined', + './defineProperties' + ], function( + Check, + defaultValue, + defined, + defineProperties) { + '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. + */ + function Heap(comparator) { + //>>includeStart('debug', pragmas.debug); + Check.defined('comparator', comparator); + //>>includeEnd('debug'); + + this._comparator = comparator; + this._array = []; + this._length = 0; + this._maximumLength = undefined; + } + + defineProperties(Heap.prototype, { + /** + * Gets the length of the heap. + * + * @memberof Heap.prototype + * + * @type {Number} + * @readonly + */ + length : { + get : function() { + return this._length; + } + }, + + /** + * Gets the internal array. + * + * @memberof Heap.prototype + * + * @type {Array} + * @readonly + */ + internalArray : { + get : function() { + return this._array; + } + }, + + /** + * Gets and sets the maximum length of the heap. + * + * @memberof Heap.prototype + * + * @type {Number} + */ + maximumLength : { + get : function() { + return this._maximumLength; + }, + set : function(value) { + this._maximumLength = value; + if (this._length > value && value > 0) { + this._length = value; + this._array.length = value; + } + } + } + }); + + 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 length of the heap. + */ + Heap.prototype.reserve = function(length) { + length = defaultValue(length, this._length); + this._array.length = length; + }; + + /** + * Update the heap so that index and all descendants satisfy the heap property. + * + * @param {Number} [index=0] The starting index to heapify from. + */ + Heap.prototype.heapify = function(index) { + index = defaultValue(index, 0); + var length = this._length; + var comparator = this._comparator; + var array = this._array; + var candidate = -1; + + while (true) { + var right = 2 * (index + 1); + var left = right - 1; + + if (left < length && comparator(array[left], array[index]) < 0) { + candidate = left; + } else { + candidate = index; + } + + if (right < length && comparator(array[right], array[candidate]) < 0) { + candidate = right; + } + if (candidate !== index) { + swap(array, candidate, index); + index = candidate; + } else { + break; + } + } + }; + + /** + * Insert an element into the heap. If the length would grow greater than maximumLength + * of the heap, extra elements are removed. + * + * @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(element) { + //>>includeStart('debug', pragmas.debug); + Check.defined('element', element); + //>>includeEnd('debug'); + + var array = this._array; + var comparator = this._comparator; + + var index = this._length++; + if (index < array.length) { + array[index] = element; + } else { + array.push(element); + } + + while (index !== 0) { + var parent = Math.floor((index - 1) / 2); + if (comparator(array[index], array[parent]) < 0) { + swap(array, index, parent); + index = parent; + } else { + break; + } + } + + var removedElement; + + if (defined(this._maximumLength) && (this._length > this._maximumLength)) { + removedElement = array[this.maximumLength]; + this._length = this._maximumLength; + } + + return removedElement; + }; + + /** + * Remove the element specified by index from the heap and return it. + * + * @param {Number} [index=0] The index to remove. + * @returns {*} The specified element of the heap. + */ + Heap.prototype.pop = function(index) { + index = defaultValue(index, 0); + if (this._length === 0) { + return undefined; + } + //>>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; + }; + + return Heap; +}); diff --git a/Source/Core/Iau2006XysData.js b/Source/Core/Iau2006XysData.js index da31fb1402e..a4d504ea6b2 100644 --- a/Source/Core/Iau2006XysData.js +++ b/Source/Core/Iau2006XysData.js @@ -244,7 +244,7 @@ define([ chunkUrl = buildModuleUrl('Assets/IAU2006_XYS/IAU2006_XYS_' + chunkIndex + '.json'); } - when(loadJson(chunkUrl), function(chunk) { + loadJson(chunkUrl, function(chunk) { xysData._chunkDownloadsInProgress[chunkIndex] = false; var samples = xysData._samples; diff --git a/Source/Core/Request.js b/Source/Core/Request.js new file mode 100644 index 00000000000..7661249d056 --- /dev/null +++ b/Source/Core/Request.js @@ -0,0 +1,162 @@ +/*global define*/ +define([ + './defaultValue', + './defined', + './defineProperties', + './RequestState', + './RequestType' + ], function( + defaultValue, + defined, + defineProperties, + RequestState, + RequestType) { + 'use strict'; + + /** + * Stores information for making a request. This should not be constructed directly. Instead, call the appropriate load function, like `loadWithXhr`, `loadImage`, etc. + * + * @alias Request + * @constructor + * + * @param {Object} [options] An object with the following properties: + * @param {Boolean} [options.url] The url to request. + * @param {Function} [options.requestFunction] The actual function that makes the request. The function takes no arguments and returns a promise for the requested data. + * @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. + * @param {Number} [options.distance=0.0] The distance from the camera, used to prioritize requests. + * @param {Number} [options.screenSpaceError=0.0] The screen space error, used to prioritize requests. + */ + 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; + + /** + * The actual function that makes the request. The function takes no arguments and returns a promise for the requested data. + * + * @type {Function} + * + * @private + */ + this.requestFunction = options.requestFunction; + + /** + * 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.throttle = throttle; + + /** + * 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.throttleByServer = throttleByServer; + + /** + * Type of request. + * + * @type {RequestType} + * @readonly + * + * @default RequestType.OTHER + */ + this.type = defaultValue(options.type, RequestType.OTHER); + + /** + * The distance from the camera, used to prioritize requests. This value may be edited continually to update + * the request's priority. + * + * @type {Number} + * + * @default 0.0 + */ + this.distance = defaultValue(options.distance, 0.0); + + /** + * The screen space error, used to prioritize requests. This value may be edited continually to update + * the request's priority. + * + * @type {Number} + * + * @default 0.0 + */ + this.screenSpaceError = defaultValue(options.screenSpaceError, 0.0); + + /** + * The request server, derived from the url. + * + * @type {String} + * + * @private + */ + this.server = undefined; + + /** + * The current state of the request. + * + * @type {RequestState} + * + * @private + */ + this.state = RequestState.UNISSUED; + + /** + * Reference to the underlying XMLHttpRequest so that it may be aborted in RequestScheduler. + * + * @type {Object} + * + * @private + */ + this.xhr = undefined; + + /** + * The requests's deferred promise. + * + * @type {Object} + * + * @private + */ + this.deferred = undefined; + + /** + * Whether the request was explicitly cancelled. + * + * @type {Boolean} + * + * @private + */ + this.cancelled = false; + } + + /** + * Mark the request as cancelled. + * + * @private + */ + Request.prototype.cancel = function() { + this.cancelled = true; + }; + + return Request; +}); diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js new file mode 100644 index 00000000000..cd8df1f3d45 --- /dev/null +++ b/Source/Core/RequestScheduler.js @@ -0,0 +1,383 @@ +/*global define*/ +define([ + './clone', + './Check', + './defined', + './defineProperties', + './Heap', + './isBlobUri', + './isDataUri', + './RequestState', + '../ThirdParty/Uri', + '../ThirdParty/when' + ], function( + clone, + Check, + defined, + defineProperties, + Heap, + isBlobUri, + isDataUri, + RequestState, + Uri, + when) { + 'use strict'; + + /** + * Tracks the number of active requests and prioritizes incoming requests. + * + * @exports RequestScheduler + * + * @private + */ + function RequestScheduler() { + } + + /** + * 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 50 + */ + RequestScheduler.maximumRequestsPerServer = 6; + + /** + * 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. + * @type {Number} + * @default 20 + */ + RequestScheduler.priorityHeapSize = 20; + + /** + * Specifies if the request scheduler should throttle incoming requests, or let the browser queue requests under its control. + * @type {Boolean} + * @default true + */ + RequestScheduler.debugThrottle = true; + + /** + * When true, log statistics to the console every frame + * @type {Boolean} + * @default false + */ + RequestScheduler.debugShowStatistics = false; + + defineProperties(RequestScheduler, { + /** + * Returns the statistics used by the request scheduler. + * + * @memberof RequestScheduler + * + * @type Object + * @readonly + */ + statistics : { + get : function() { + return statistics; + } + } + }); + + function sortRequests(a, b) { + return a.distance - b.distance; + } + + var statistics = { + numberOfAttemptedRequests : 0, + numberOfActiveRequests : 0, + numberOfCancelledRequests : 0, + numberOfCancelledActiveRequests : 0, + numberOfFailedRequests : 0, + numberOfActiveRequestsEver : 0 + }; + + var requestHeap = new Heap(sortRequests); + requestHeap.maximumLength = RequestScheduler.priorityHeapSize; + requestHeap.reserve(RequestScheduler.priorityHeapSize); + var activeRequests = []; + + var numberOfActiveRequestsByServer = {}; + + var pageUri = typeof document !== 'undefined' ? new Uri(document.location.href) : new Uri(); + + /** + * Get the server name from a given url. + * + * @param {String} url The url. + * @returns {String} The server name. + */ + RequestScheduler.getServer = function(url) { + //>>includeStart('debug', pragmas.debug); + 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 length = numberOfActiveRequestsByServer[serverName]; + if (!defined(length)) { + numberOfActiveRequestsByServer[serverName] = 0; + } + + return serverName; + }; + + /** + * For testing only. + * + * @private + */ + RequestScheduler.clearForSpecs = function() { + var i; + var length = activeRequests.length; + for (i = 0; i < length; ++i) { + activeRequests[i].cancel(); + } + length = requestHeap.length; + for (i = 0; i < length; ++i) { + requestHeap.internalArray[i].cancel(); + } + + RequestScheduler.update(); + + // Clear stats + statistics.numberOfAttemptedRequests = 0; + statistics.numberOfActiveRequests = 0; + statistics.numberOfCancelledRequests = 0; + statistics.numberOfCancelledActiveRequests = 0; + statistics.numberOfFailedRequests = 0; + statistics.numberOfActiveRequestsEver = 0; + }; + + /** + * For testing only. + * + * @private + */ + RequestScheduler.numberOfActiveRequestsByServer = function(serverName) { + return numberOfActiveRequestsByServer[serverName]; + }; + + function serverHasOpenSlots(server) { + return numberOfActiveRequestsByServer[server] < RequestScheduler.maximumRequestsPerServer; + } + + function issueRequest(request) { + if (request.state === RequestState.UNISSUED) { + request.state = RequestState.ISSUED; + request.deferred = when.defer(); + } + return request.deferred.promise; + } + + function getRequestReceivedFunction(request) { + return function(results) { + if (request.state === RequestState.CANCELLED) { + return; + } + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.server]; + request.state = RequestState.RECEIVED; + request.deferred.resolve(results); + }; + } + + function getRequestFailedFunction(request) { + return function(error) { + if (request.state === RequestState.CANCELLED) { + return; + } + ++statistics.numberOfFailedRequests; + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.server]; + request.state = RequestState.FAILED; + request.deferred.reject(error); + }; + } + + function startRequest(request, passthrough) { + if (passthrough) { + // Data uri or blob uri should always succeed so make the request immediately and don't contribute to statistics. + request.state = RequestState.RECEIVED; + return request.requestFunction(); + } + + var promise = issueRequest(request); + request.state = RequestState.ACTIVE; + activeRequests.push(request); + ++statistics.numberOfActiveRequests; + ++statistics.numberOfActiveRequestsEver; + ++numberOfActiveRequestsByServer[request.server]; + + request.requestFunction().then(getRequestReceivedFunction(request)).otherwise(getRequestFailedFunction(request)); + return promise; + } + + function cancelRequest(request) { + if (request.state === RequestState.ACTIVE) { + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.server]; + ++statistics.numberOfCancelledActiveRequests; + if (defined(request.xhr)) { + // TODO : make sure this doesn't trigger a failed promise, if so the deferred can be rejected first + // TODO : test this in a test + request.xhr.abort(); + } + } + request.state = RequestState.CANCELLED; + ++statistics.numberOfCancelledRequests; + request.deferred.reject('Cancelled'); + } + + /** + * Issuers of a request should update properties of requests. At the end of the frame, + * RequestScheduler.update is called to start, cancel, or defer requests. + */ + RequestScheduler.update = function() { + var request; + + // Loop over all active requests. Cancelled, failed, or received requests are removed from the array to make room for new requests. + // If an active request is cancelled, its XMLHttpRequest will be aborted. + var removeCount = 0; + var activeLength = activeRequests.length; + for (var 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; + + // Resort the heap since priority may have changed. Distance and sse are updated prior to getting here. + requestHeap.heapify(); + + // 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; + } + + if (request.throttleByServer && !serverHasOpenSlots(request.server)) { + // Open slots are available, but the request is throttled by its server. Cancel and try again later. + cancelRequest(request); + continue; + } + + startRequest(request); + ++filledSlots; + } + + updateStatistics(); + }; + + /** + * 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 {Request} request The request object. + * + * @returns {Promise|undefined} A Promise for the requested data, or undefined if this request does not have high enough priority to be issued. + */ + 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'); + + ++statistics.numberOfAttemptedRequests; + + var isDataOrBlobUri = isDataUri(request.url) || isBlobUri(request.url); + if (!isDataOrBlobUri && !defined(request.server)) { + request.server = RequestScheduler.getServer(request.url); + } + + if (!RequestScheduler.debugThrottle || !request.throttle || isDataOrBlobUri) { + return startRequest(request, isDataOrBlobUri); + } + + if (activeRequests.length >= RequestScheduler.maximumRequests) { + // Active requests are saturated. Try again later. + return undefined; + } + + if (request.throttleByServer && !serverHasOpenSlots(request.server)) { + // Server is saturated. Try again later. + return undefined; + } + + // Insert into the priority heap and see if a request was bumped off. If this request is the lowest + // priority it will be returned. + var removedRequest = requestHeap.insert(request); + + if (defined(removedRequest)) { + if (removedRequest === request) { + // Request does not have high enough priority to be issued + return undefined; + } + // A previously issued request has been bumped off the priority heap, so cancel it + cancelRequest(removedRequest); + } + + return issueRequest(request); + }; + + function clearStatistics() { + statistics.numberOfAttemptedRequests = 0; + statistics.numberOfCancelledRequests = 0; + statistics.numberOfCancelledActiveRequests = 0; + } + + function updateStatistics() { + if (!RequestScheduler.debugShowStatistics) { + return; + } + + if (statistics.numberOfAttemptedRequests > 0) { + console.log('Number of attempted requests: ' + statistics.numberOfAttemptedRequests); + } + 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(); + } + + return RequestScheduler; +}); diff --git a/Source/Core/RequestState.js b/Source/Core/RequestState.js new file mode 100644 index 00000000000..77aea959ffb --- /dev/null +++ b/Source/Core/RequestState.js @@ -0,0 +1,21 @@ +/*global define*/ +define([ + '../Core/freezeObject' +], function( + freezeObject) { + 'use strict'; + + /** + * @private + */ + var RequestState = { + UNISSUED : 0, // Initial unissued state. + ISSUED : 1, // Issued but not yet active. Will become active when open slots are available. + ACTIVE : 2, // Actual http request has been sent. + RECEIVED : 3, // Request completed successfully. + CANCELLED : 4, // Request was cancelled, either explicitly or automatically because of low priority. + FAILED : 5 // Request failed. + }; + + return freezeObject(RequestState); +}); diff --git a/Source/Core/RequestType.js b/Source/Core/RequestType.js new file mode 100644 index 00000000000..9beebd2b238 --- /dev/null +++ b/Source/Core/RequestType.js @@ -0,0 +1,21 @@ +/*global define*/ +define([ + '../Core/freezeObject' + ], function( + freezeObject) { + 'use strict'; + + /** + * An enum identifying the type of request. Used for finer grained logging and priority sorting. + * + * @exports RequestType + */ + var RequestType = { + TERRAIN : 0, + IMAGERY : 1, + TILES3D : 2, + OTHER : 3 + }; + + return freezeObject(RequestType); +}); diff --git a/Source/Core/TerrainProvider.js b/Source/Core/TerrainProvider.js index 5163bae2986..bfba9cf039d 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. + * * @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..79523b4afe6 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. * @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.36.'); + 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..d86038eff30 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. + * @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..aad892afdbd 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. + * @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..f01270bb6ff 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. + * @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..b726ed80056 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. + * @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 Cesium 1.36'); + 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..7f2e12ecc23 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. + * @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..f3215e59448 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. + * @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..5e721769340 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. + * @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..9be69bb2bb8 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. + * @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..a92607405ed 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. + * @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..d2102b50bac 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,40 @@ 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.36'); + 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); - + request.xhr = loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); return deferred.promise; - }); + }; + + return RequestScheduler.request(request); } var dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/; @@ -192,6 +217,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..e43ef006436 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. + * @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..0970ce28b2e 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. * @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..3dea11dc2da 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. * @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/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index d386273ab93..90b0da7a002 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -237,12 +237,33 @@ 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 + }); + } + 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); } + // Update distance while the tile loads + tile._distance = surfaceTile.tileBoundingRegion.distanceToCamera(frameState); + if (tile.state === QuadtreeTileLoadState.START) { prepareNewTile(tile, terrainProvider, imageryLayerCollection); tile.state = QuadtreeTileLoadState.LOADING; @@ -339,7 +360,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._distance); // 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..79c0f4d56df 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. * @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, + distance : request.distance + }); + 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..ab4adf47350 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. * @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 f91a379d720..010d653db2b 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..b98e628a84a 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. * @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..d832d9f210a 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,15 @@ define([ return this.referenceCount; }; - Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection) { + Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, distance) { if (this.state === ImageryState.UNLOADED) { this.state = ImageryState.TRANSITIONING; - this.imageryLayer._requestImagery(this); + this.imageryLayer._requestImagery(this, distance); + } + + if (defined(this.request) && this.request.state === RequestState.ISSUED) { + // Update distance while loading to prioritize request + this.request.distance = distance; } if (this.state === ImageryState.RECEIVED) { diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js index 9fef8eeac77..038826aaea0 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 {Number} [distance] The distance of the tile from the camera. */ - ImageryLayer.prototype._requestImagery = function(imagery) { + ImageryLayer.prototype._requestImagery = function(imagery, distance) { 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, + distance : distance + }); + 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..b73b5adfddb 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. * @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. * @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..5c32d2082b6 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. * @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/Scene.js b/Source/Scene/Scene.js index a09dbd9dedd..79dae912521 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -29,6 +29,7 @@ define([ '../Core/mergeSort', '../Core/Occluder', '../Core/PixelFormat', + '../Core/RequestScheduler', '../Core/ShowGeometryInstanceAttribute', '../Core/Transforms', '../Renderer/ClearCommand', @@ -104,6 +105,7 @@ define([ mergeSort, Occluder, PixelFormat, + RequestScheduler, ShowGeometryInstanceAttribute, Transforms, ClearCommand, @@ -2639,6 +2641,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..ca9486e0f1f 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. * @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..52e4ebc2b2b 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. * @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..282b44cda6a 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._distance); 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._distance); 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..8e1a4b12c0f 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,19 @@ 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, distance) { if (this.state === TerrainState.UNLOADED) { - requestTileGeometry(this, terrainProvider, x, y, level); + requestTileGeometry(this, terrainProvider, x, y, level, distance); + } + + if (defined(this.request) && this.request.state === RequestState.ISSUED) { + // Update distance while loading to prioritize request + this.request.distance = distance; } if (this.state === TerrainState.RECEIVED) { @@ -106,16 +112,26 @@ define([ } }; - function requestTileGeometry(tileTerrain, terrainProvider, x, y, level) { + function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, distance) { 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 +145,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, + distance : distance + }); + 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..5ab353eab4e 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. * @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..93f3c83dc43 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. * @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..2da7cd3fb36 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. * @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/Core/CesiumTerrainProviderSpec.js b/Specs/Core/CesiumTerrainProviderSpec.js index e479b6011e0..27bc94a4730 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,6 +24,8 @@ defineSuite([ loadWithXhr, CesiumMath, QuantizedMeshTerrainData, + Request, + RequestScheduler, TerrainProvider, pollToPromise, when) { @@ -84,6 +88,12 @@ defineSuite([ }); } + function createRequest() { + return new Request({ + throttleByServer : true + }); + } + it('conforms to TerrainProvider interface', function() { expect(CesiumTerrainProvider).toConformToInterface(TerrainProvider); }); @@ -576,15 +586,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..024d4a23e34 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,12 @@ defineSuite([ }); } + function createRequest() { + return new Request({ + throttleByServer : true + }); + } + afterEach(function() { loadWithXhr.load = loadWithXhr.defaultLoad; }); @@ -304,17 +314,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 new file mode 100644 index 00000000000..d4ef176e6d9 --- /dev/null +++ b/Specs/Core/HeapSpec.js @@ -0,0 +1,119 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/Heap' +], function( + Heap) { + 'use strict'; + + var length = 100; + + function checkHeap(heap, comparator) { + var array = heap.internalArray; + var pass = true; + 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(array[i], array[left]) <= 0); + } + if (right < heap.length) { + pass = pass && (comparator(array[i], array[right]) <= 0); + } + } + return pass; + } + + // min heap + function comparator(a, b) { + return a - b; + } + + it('maintains heap property on insert', function() { + var heap = new Heap(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 i; + for (i = 0; i < length; ++i) { + heap.insert(Math.random()); + } + var pass = true; + for (i = 0; i < length; ++i) { + heap.pop(); + pass = pass && checkHeap(heap, comparator); + } + expect(pass).toBe(true); + }); + + it('limited by maximum length', function() { + var heap = new Heap(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.maximumLength).toBe(true); + // allowed one extra slot for swapping + expect(heap.internalArray.length).toBeLessThanOrEqualTo(heap.maximumLength + 1); + }); + + it('pops in sorted order', function() { + var heap = new Heap(comparator); + var i; + for (i = 0; i < length; ++i) { + heap.insert(Math.random()); + } + var curr = heap.pop(); + var pass = true; + for (i = 0; i < length - 1; ++i) { + var next = heap.pop(); + pass = pass && (comparator(curr, next) <= 0); + curr = next; + } + expect(pass).toBe(true); + }); + + it('insert returns the removed element when maximumLength is set', function() { + var heap = new Heap(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); + }); +}); diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js new file mode 100644 index 00000000000..d23bb5ecab1 --- /dev/null +++ b/Specs/Core/RequestSchedulerSpec.js @@ -0,0 +1,304 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/RequestScheduler', + 'Core/Request', + 'Core/RequestState', + 'Core/RequestType', + 'ThirdParty/when' + ], function( + RequestScheduler, + Request, + RequestState, + RequestType, + when) { + 'use strict'; + + var originalMaximumRequests; + var originalMaximumRequestsPerServer; + + beforeEach(function() { + originalMaximumRequests = RequestScheduler.maximumRequests; + originalMaximumRequestsPerServer = RequestScheduler.maximumRequestsPerServer; + }); + + afterEach(function() { + RequestScheduler.maximumRequests = originalMaximumRequests; + RequestScheduler.maximumRequestsPerServer = originalMaximumRequestsPerServer; + }); + + it('request throws when request is undefined', function() { + expect(function() { + RequestScheduler.request(); + }).toThrowDeveloperError(); + }); + + it('request throws when request.url is undefined', function() { + expect(function() { + RequestScheduler.request(new Request({ + requestFunction : function(url) { + return undefined; + } + })); + }).toThrowDeveloperError(); + }); + + it('request throws when request.requestFunction is undefined', function() { + expect(function() { + RequestScheduler.request(new Request({ + url : 'file/path' + })); + }).toThrowDeveloperError(); + }); + + it('getServer throws if url is undefined', function() { + expect(function() { + RequestScheduler.getServer(); + }).toThrowDeveloperError(); + }); + + it('getServer with https', function() { + var server = RequestScheduler.getServer('https://foo.com/1'); + expect(server).toEqual('foo.com:443'); + }); + + it('getServer with http', function() { + var server = RequestScheduler.getServer('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() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + 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(); + + // Scheduler is full, promise3 will be undefined + var promise3 = RequestScheduler.request(createRequest()); + RequestScheduler.update(); + + 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(); + + expect(statistics.numberOfActiveRequests).toBe(2); + expect(promise4).toBeDefined(); + + // 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.request(createRequest()); + RequestScheduler.update(); + + expect(statistics.numberOfActiveRequests).toBe(3); + expect(promise6).toBeDefined(); + }); + + it('honors maximumRequestsPerServer', function() { + RequestScheduler.maximumRequestsPerServer = 2; + + var deferreds = []; + + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + var url = 'http://foo.com/1'; + var server = RequestScheduler.getServer(url); + + 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(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(); + + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(2); + expect(promise4).toBeDefined(); + + // 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.request(createRequest()); + RequestScheduler.update(); + + expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(3); + expect(promise6).toBeDefined(); + }); + + function testImmediateRequest(url, dataOrBlobUri) { + var statistics = RequestScheduler.statistics; + var deferreds = []; + + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + var request = new Request({ + url : url, + requestFunction : requestFunction + }); + + var promise = RequestScheduler.request(request); + expect(promise).toBeDefined(); + + if (dataOrBlobUri) { + expect(request.server).toBeUndefined(); + expect(statistics.numberOfActiveRequests).toBe(0); + } else { + expect(statistics.numberOfActiveRequests).toBe(1); + expect(RequestScheduler.numberOfActiveRequestsByServer(request.server)).toBe(1); + } + + deferreds[0].resolve(); + + return promise.then(function() { + expect(request.state).toBe(RequestState.RECEIVED); + expect(statistics.numberOfActiveRequests).toBe(0); + if (!dataOrBlobUri) { + expect(RequestScheduler.numberOfActiveRequestsByServer(request.server)).toBe(0); + } + }); + } + + it('data uri goes through immediately', function() { + var dataUri = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D'; + testImmediateRequest(dataUri, true); + }); + + it('blob uri goes through immediately', function() { + var uint8Array = new Uint8Array(4); + var blob = new Blob([uint8Array], { + type : 'application/octet-stream' + }); + + var blobUrl = window.URL.createObjectURL(blob); + testImmediateRequest(blobUrl, true); + }); + + it('request goes through immediately when throttle is false', function() { + var url = 'https://foo.com/1'; + testImmediateRequest(url, false); + }); + + it('debugThrottle', function() { + RequestScheduler.maximumRequests = 0; + + function requestFunction() { + return when.resolve(); + } + + RequestScheduler.debugThrottle = true; + var request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + var promise = RequestScheduler.request(request); + expect(promise).toBeUndefined(); + + RequestScheduler.debugThrottle = false; + request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + promise = RequestScheduler.request(request); + expect(promise).toBeDefined(); + }); + + // it('sorts requests by distance', function() { + // var currentDistance = 0.0; + // + // function getRequestFunction(distance) { + // return function() { + // expect(distance).toBeGreaterThan(currentDistance); + // currentDistance = distance; + // return when.resolve(); + // }; + // } + // + // function createRequest(distance) { + // return new Request({ + // throttle : true, + // url : 'https://foo.com/1', + // requestFunction : getRequestFunction(distance), + // distance : distance + // }); + // } + // + // var length = RequestScheduler.priorityHeapSize; + // for (var i = 0; i < length; ++i) { + // var distance = Math.random(); + // RequestScheduler.request(createRequest(distance)); + // } + // + // RequestScheduler.update(); + // }); +}); diff --git a/Specs/Core/VRTheWorldTerrainProviderSpec.js b/Specs/Core/VRTheWorldTerrainProviderSpec.js index f04ccee3a01..205d7c317b6 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,6 +20,8 @@ defineSuite([ loadImage, loadWithXhr, CesiumMath, + Request, + RequestScheduler, TerrainProvider, pollToPromise, when) { @@ -58,6 +62,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 +283,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/Core/throttleRequestByServerSpec.js b/Specs/Core/throttleRequestByServerSpec.js deleted file mode 100644 index a8457fa9093..00000000000 --- a/Specs/Core/throttleRequestByServerSpec.js +++ /dev/null @@ -1,59 +0,0 @@ -/*global defineSuite*/ -defineSuite([ - 'Core/throttleRequestByServer', - 'ThirdParty/when' - ], function( - throttleRequestByServer, - when) { - 'use strict'; - - var originalMaximumRequestsPerServer; - - beforeEach(function() { - originalMaximumRequestsPerServer = throttleRequestByServer.maximumRequestsPerServer; - }); - - afterEach(function() { - throttleRequestByServer.maximumRequestsPerServer = originalMaximumRequestsPerServer; - }); - - it('honors maximumRequestsPerServer', function() { - throttleRequestByServer.maximumRequestsPerServer = 2; - - var deferreds = []; - - function requestFunction(url) { - var deferred = when.defer(); - deferreds.push(deferred); - return deferred.promise; - } - - var promise1 = throttleRequestByServer('http://foo.com/1', requestFunction); - var promise2 = throttleRequestByServer('http://foo.com/2', requestFunction); - var promise3 = throttleRequestByServer('http://foo.com/3', requestFunction); - - expect(deferreds.length).toBe(2); - expect(promise1).toBeDefined(); - expect(promise2).toBeDefined(); - expect(promise3).not.toBeDefined(); - - deferreds[0].resolve(); - - var promise4 = throttleRequestByServer('http://foo.com/3', requestFunction); - expect(deferreds.length).toBe(3); - expect(promise4).toBeDefined(); - - var promise5 = throttleRequestByServer('http://foo.com/4', requestFunction); - expect(deferreds.length).toBe(3); - expect(promise5).not.toBeDefined(); - - throttleRequestByServer.maximumRequestsPerServer = 3; - var promise6 = throttleRequestByServer('http://foo.com/4', requestFunction); - expect(deferreds.length).toBe(4); - expect(promise6).toBeDefined(); - - deferreds[1].resolve(); - deferreds[2].resolve(); - deferreds[3].resolve(); - }); -}); diff --git a/Specs/Scene/ArcGisMapServerImageryProviderSpec.js b/Specs/Scene/ArcGisMapServerImageryProviderSpec.js index 14276f52a1f..1d5d0165307 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, @@ -602,6 +604,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -622,6 +627,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..f148419fa40 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, @@ -364,6 +366,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -399,6 +404,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/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..05cccff9bf6 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, @@ -259,6 +261,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 +284,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..cab813d0d41 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, @@ -250,6 +252,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -270,6 +275,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..dedb6d22d66 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, @@ -204,6 +206,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -224,6 +229,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 d55572ed361..19cf18665c2 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, @@ -700,6 +702,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -717,6 +722,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..239b524c8fa 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, @@ -361,6 +363,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -381,6 +386,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..656b1e378fa 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, @@ -223,6 +225,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -243,6 +248,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..9fa7e58669e 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, @@ -360,6 +362,9 @@ defineSuite([ if (tries < 3) { error.retry = true; } + setTimeout(function() { + RequestScheduler.update(); + }, 1); }); loadImage.createImage = function(url, crossOrigin, deferred) { @@ -380,6 +385,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/customizeJasmine.js b/Specs/customizeJasmine.js index e857b01912a..e999849c95a 100644 --- a/Specs/customizeJasmine.js +++ b/Specs/customizeJasmine.js @@ -9,7 +9,7 @@ define([ equalsMethodEqualityTester) { "use strict"; - return function (env, includedCategory, excludedCategory, webglValidation, webglStub, release) { + return function (env, includedCategory, excludedCategory, webglValidation, webglStub, release, RequestScheduler) { function defineSuite(deps, name, suite, categories, focus) { /*global define,describe,fdescribe*/ if (typeof suite === 'object' || typeof suite === 'string') { @@ -72,6 +72,8 @@ define([ window.it = function(description, f, timeout, categories) { originalIt(description, function(done) { + // Clear the RequestScheduler before for each test in case requests are still active from previous tests + RequestScheduler.clearForSpecs(); var result = f(); when(result, function() { done(); diff --git a/Specs/karma-main.js b/Specs/karma-main.js index 69575ab18f1..e7fc78ff3cc 100644 --- a/Specs/karma-main.js +++ b/Specs/karma-main.js @@ -53,11 +53,12 @@ } require([ + 'Core/RequestScheduler', 'Specs/customizeJasmine' ], function( + RequestScheduler, customizeJasmine) { - - customizeJasmine(jasmine.getEnv(), included, excluded, webglValidation, webglStub, release); + customizeJasmine(jasmine.getEnv(), included, excluded, webglValidation, webglStub, release, RequestScheduler); var specFiles = Object.keys(__karma__.files).filter(function(file) { return /Spec\.js$/.test(file); diff --git a/Specs/spec-main.js b/Specs/spec-main.js index 9ba3c174eee..fb4e707f5ba 100644 --- a/Specs/spec-main.js +++ b/Specs/spec-main.js @@ -136,6 +136,8 @@ window.it = function(description, f, timeout, categories) { originalIt(description, function(done) { + // Clear the RequestScheduler before for each test in case requests are still active from previous tests + Cesium.RequestScheduler.clearForSpecs(); var result = f(); when(result, function() { done(); From 3f38546f6b6b5ffab479f9e29711e4ae5ba8e086 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 1 Jun 2017 20:25:54 -0400 Subject: [PATCH 03/41] Fix mistake from my testing --- Source/Core/Iau2006XysData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/Iau2006XysData.js b/Source/Core/Iau2006XysData.js index a4d504ea6b2..da31fb1402e 100644 --- a/Source/Core/Iau2006XysData.js +++ b/Source/Core/Iau2006XysData.js @@ -244,7 +244,7 @@ define([ chunkUrl = buildModuleUrl('Assets/IAU2006_XYS/IAU2006_XYS_' + chunkIndex + '.json'); } - loadJson(chunkUrl, function(chunk) { + when(loadJson(chunkUrl), function(chunk) { xysData._chunkDownloadsInProgress[chunkIndex] = false; var samples = xysData._samples; From a1cc29b5bdb30d447b278e95d876a881de6f09f0 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 1 Jun 2017 22:27:35 -0400 Subject: [PATCH 04/41] More tests and slightly different order-of-operations for cancelled requests which removes the TODO there --- Source/Core/RequestScheduler.js | 33 ++++--- Specs/Core/RequestSchedulerSpec.js | 189 +++++++++++++++++++++++++++++++------ 2 files changed, 177 insertions(+), 45 deletions(-) diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index cd8df1f3d45..34870fa5cf7 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -205,13 +205,7 @@ define([ }; } - function startRequest(request, passthrough) { - if (passthrough) { - // Data uri or blob uri should always succeed so make the request immediately and don't contribute to statistics. - request.state = RequestState.RECEIVED; - return request.requestFunction(); - } - + function startRequest(request) { var promise = issueRequest(request); request.state = RequestState.ACTIVE; activeRequests.push(request); @@ -224,19 +218,21 @@ define([ } function cancelRequest(request) { - if (request.state === RequestState.ACTIVE) { + var active = request.state === RequestState.ACTIVE; + request.state = RequestState.CANCELLED; + ++statistics.numberOfCancelledRequests; + request.deferred.reject('Cancelled'); + + if (active) { + // Despite the Request being cancelled, the xhr request is still in flight. + // When it resolves getRequestReceivedFunction or getRequestFailedFunction will ignore it. --statistics.numberOfActiveRequests; --numberOfActiveRequestsByServer[request.server]; ++statistics.numberOfCancelledActiveRequests; if (defined(request.xhr)) { - // TODO : make sure this doesn't trigger a failed promise, if so the deferred can be rejected first - // TODO : test this in a test request.xhr.abort(); } } - request.state = RequestState.CANCELLED; - ++statistics.numberOfCancelledRequests; - request.deferred.reject('Cancelled'); } /** @@ -312,15 +308,18 @@ define([ Check.typeOf.func('request.requestFunction', request.requestFunction); //>>includeEnd('debug'); + if (isDataUri(request.url) || isBlobUri(request.url)) { + return request.requestFunction(); + } + ++statistics.numberOfAttemptedRequests; - var isDataOrBlobUri = isDataUri(request.url) || isBlobUri(request.url); - if (!isDataOrBlobUri && !defined(request.server)) { + if (!defined(request.server)) { request.server = RequestScheduler.getServer(request.url); } - if (!RequestScheduler.debugThrottle || !request.throttle || isDataOrBlobUri) { - return startRequest(request, isDataOrBlobUri); + if (!RequestScheduler.debugThrottle || !request.throttle) { + return startRequest(request); } if (activeRequests.length >= RequestScheduler.maximumRequests) { diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index d23bb5ecab1..262bbb936a5 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -247,6 +247,103 @@ defineSuite([ testImmediateRequest(url, false); }); + it('makes a throttled request', function() { + var deferreds = []; + + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + 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); + + 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 + }); + + var promise = RequestScheduler.request(request); + expect(request.state).toBe(RequestState.ISSUED); + + request.cancel(); + RequestScheduler.update(); + + expect(request.state).toBe(RequestState.CANCELLED); + expect(statistics.numberOfCancelledRequests).toBe(1); + expect(statistics.numberOfCancelledActiveRequests).toBe(0); + + return promise.then(function() { + fail('should not be called'); + }).otherwise(function(error) { + expect(error).toBe('Cancelled'); + }); + }); + + it('cancels an active request', function() { + var statistics = RequestScheduler.statistics; + var aborted = true; + var mockXhr = { + abort : function() { + aborted = true; + } + }; + + function requestFunction() { + request.xhr = mockXhr; + return when.defer().promise; + } + + var request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + + 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.server)).toBe(0); + expect(aborted).toBe(true); + + return promise.then(function() { + fail('should not be called'); + }).otherwise(function(error) { + expect(error).toBe('Cancelled'); + }); + }); + it('debugThrottle', function() { RequestScheduler.maximumRequests = 0; @@ -271,34 +368,70 @@ defineSuite([ }); promise = RequestScheduler.request(request); expect(promise).toBeDefined(); + + RequestScheduler.debugThrottle = true; }); - // it('sorts requests by distance', function() { - // var currentDistance = 0.0; - // - // function getRequestFunction(distance) { - // return function() { - // expect(distance).toBeGreaterThan(currentDistance); - // currentDistance = distance; - // return when.resolve(); - // }; - // } - // - // function createRequest(distance) { - // return new Request({ - // throttle : true, - // url : 'https://foo.com/1', - // requestFunction : getRequestFunction(distance), - // distance : distance - // }); - // } - // - // var length = RequestScheduler.priorityHeapSize; - // for (var i = 0; i < length; ++i) { - // var distance = Math.random(); - // RequestScheduler.request(createRequest(distance)); - // } - // - // RequestScheduler.update(); - // }); + it('prioritizes requests by distance', function() { + var currentDistance = 0.0; + + function getRequestFunction(distance) { + return function() { + expect(distance).toBeGreaterThan(currentDistance); + currentDistance = distance; + return when.resolve(); + }; + } + + function createRequest(distance) { + return new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : getRequestFunction(distance), + distance : distance + }); + } + + var length = RequestScheduler.priorityHeapSize; + for (var i = 0; i < length; ++i) { + var distance = Math.random(); + RequestScheduler.request(createRequest(distance)); + } + + RequestScheduler.update(); + }); + + it('handles low priority requests', function() { + function requestFunction() { + return when.resolve(); + } + + function createRequest(distance) { + return new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction, + distance : distance + }); + } + + var highPriority = 0.0; + var mediumPriority = 0.5; + var lowPriority = 1.0; + + var length = RequestScheduler.priorityHeapSize; + for (var i = 0; i < length; ++i) { + RequestScheduler.request(createRequest(mediumPriority)); + } + + // 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); + + // 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); + }); }); From 172da974c1826eebcd8506caa5f2eeda343f4bee Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 2 Jun 2017 16:52:24 -0400 Subject: [PATCH 05/41] More tests and fix re-sorting priority --- Source/Core/Heap.js | 10 ++ Source/Core/RequestScheduler.js | 87 ++++++++----- Specs/Core/HeapSpec.js | 44 +++++++ Specs/Core/RequestSchedulerSpec.js | 254 ++++++++++++++++++++++++++++++++++--- 4 files changed, 348 insertions(+), 47 deletions(-) diff --git a/Source/Core/Heap.js b/Source/Core/Heap.js index 77472566b2c..a455ce6d3cd 100644 --- a/Source/Core/Heap.js +++ b/Source/Core/Heap.js @@ -132,6 +132,16 @@ define([ }; /** + * Resort the heap. + */ + 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 maximumLength * of the heap, extra elements are removed. * diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 34870fa5cf7..13301e4b474 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -23,6 +23,30 @@ define([ when) { 'use strict'; + function sortRequests(a, b) { + return a.distance - b.distance; + } + + var statistics = { + numberOfAttemptedRequests : 0, + numberOfActiveRequests : 0, + numberOfCancelledRequests : 0, + numberOfCancelledActiveRequests : 0, + numberOfFailedRequests : 0, + numberOfActiveRequestsEver : 0 + }; + + var priorityHeapLength = 20; + var requestHeap = new Heap(sortRequests); + requestHeap.maximumLength = priorityHeapLength; + requestHeap.reserve(priorityHeapLength); + 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. * @@ -48,13 +72,6 @@ define([ RequestScheduler.maximumRequestsPerServer = 6; /** - * 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. - * @type {Number} - * @default 20 - */ - RequestScheduler.priorityHeapSize = 20; - - /** * Specifies if the request scheduler should throttle incoming requests, or let the browser queue requests under its control. * @type {Boolean} * @default true @@ -81,31 +98,37 @@ define([ get : function() { return statistics; } + }, + + /** + * 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 + */ + 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 (priorityHeapLength.length > value) { + var request = priorityHeapLength.pop(); + request.cancel(); + } + RequestScheduler.update(); + } + priorityHeapLength = value; + requestHeap.maximumLength = value; + requestHeap.reserve(value); + } } }); - function sortRequests(a, b) { - return a.distance - b.distance; - } - - var statistics = { - numberOfAttemptedRequests : 0, - numberOfActiveRequests : 0, - numberOfCancelledRequests : 0, - numberOfCancelledActiveRequests : 0, - numberOfFailedRequests : 0, - numberOfActiveRequestsEver : 0 - }; - - var requestHeap = new Heap(sortRequests); - requestHeap.maximumLength = RequestScheduler.priorityHeapSize; - requestHeap.reserve(RequestScheduler.priorityHeapSize); - var activeRequests = []; - - var numberOfActiveRequestsByServer = {}; - - var pageUri = typeof document !== 'undefined' ? new Uri(document.location.href) : new Uri(); - /** * Get the server name from a given url. * @@ -265,7 +288,7 @@ define([ activeRequests.length -= removeCount; // Resort the heap since priority may have changed. Distance and sse are updated prior to getting here. - requestHeap.heapify(); + 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 @@ -309,6 +332,7 @@ define([ //>>includeEnd('debug'); if (isDataUri(request.url) || isBlobUri(request.url)) { + request.state = RequestState.RECEIVED; return request.requestFunction(); } @@ -378,5 +402,8 @@ define([ clearStatistics(); } + // For testing + RequestScheduler._requestHeap = requestHeap; + return RequestScheduler; }); diff --git a/Specs/Core/HeapSpec.js b/Specs/Core/HeapSpec.js index d4ef176e6d9..a3af65f7184 100644 --- a/Specs/Core/HeapSpec.js +++ b/Specs/Core/HeapSpec.js @@ -116,4 +116,48 @@ defineSuite([ 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); + 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 262bbb936a5..c4b4b5005b1 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -190,6 +190,43 @@ defineSuite([ expect(promise6).toBeDefined(); }); + it('honors priorityHeapLength', function() { + var deferreds = []; + var requests = []; + + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + function createRequest(distance) { + var request = new Request({ + url : 'http://foo.com/1', + requestFunction : requestFunction, + throttle : true, + distance : distance + }); + requests.push(request); + return request; + } + + 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(1.0)); + expect(promise).toBeDefined(); + + // A request is cancelled to accommodate the new heap length + RequestScheduler.priorityHeapLength = 2; + expect(firstRequest.state).toBe(RequestState.CANCELLED); + }); + function testImmediateRequest(url, dataOrBlobUri) { var statistics = RequestScheduler.statistics; var deferreds = []; @@ -344,32 +381,34 @@ defineSuite([ }); }); - it('debugThrottle', function() { - RequestScheduler.maximumRequests = 0; + it('handles request failure', function() { + var statistics = RequestScheduler.statistics; + var deferreds = []; function requestFunction() { - return when.resolve(); + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; } - RequestScheduler.debugThrottle = true; var request = new Request({ - throttle : true, url : 'https://foo.com/1', requestFunction : requestFunction }); + var promise = RequestScheduler.request(request); - expect(promise).toBeUndefined(); + expect(request.state).toBe(RequestState.ACTIVE); + expect(statistics.numberOfActiveRequests).toBe(1); - RequestScheduler.debugThrottle = false; - request = new Request({ - throttle : true, - url : 'https://foo.com/1', - requestFunction : requestFunction - }); - promise = RequestScheduler.request(request); - expect(promise).toBeDefined(); + deferreds[0].reject('Request failed'); + RequestScheduler.update(); + expect(statistics.numberOfActiveRequests).toBe(0); - RequestScheduler.debugThrottle = true; + return promise.then(function() { + fail('should not be called'); + }).otherwise(function(error) { + expect(error).toBe('Request failed'); + }); }); it('prioritizes requests by distance', function() { @@ -392,13 +431,67 @@ defineSuite([ }); } - var length = RequestScheduler.priorityHeapSize; + var length = RequestScheduler.priorityHeapLength; for (var i = 0; i < length; ++i) { var distance = Math.random(); RequestScheduler.request(createRequest(distance)); } RequestScheduler.update(); + expect(currentDistance).toBeGreaterThan(0.0); // Ensures that the expect in getRequestFunction is actually called + }); + + it('updates priority', function() { + function requestFunction() { + return when.resolve(); + } + + function createRequest(distance) { + return new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction, + distance : distance + }); + } + + var i; + var request; + var length = RequestScheduler.priorityHeapLength; + for (i = 0; i < length; ++i) { + var distance = i / (length - 1); + request = createRequest(distance); + request.testId = i; + RequestScheduler.request(request); + } + + 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; + } + + for (i = 0; i < length; ++i) { + requestHeap.insert(requests[i]); + } + + for (i = 0; i < length; ++i) { + requests[i].distance = 1.0 - requests[i].distance; // Invert priority + } + + RequestScheduler.update(); + while (requestHeap.length > 0) { + request = requestHeap.pop(); + expect(request.testId).toBeLessThanOrEqualTo(currentTestId); + currentTestId = request.testId; + } }); it('handles low priority requests', function() { @@ -419,7 +512,7 @@ defineSuite([ var mediumPriority = 0.5; var lowPriority = 1.0; - var length = RequestScheduler.priorityHeapSize; + var length = RequestScheduler.priorityHeapLength; for (var i = 0; i < length; ++i) { RequestScheduler.request(createRequest(mediumPriority)); } @@ -434,4 +527,131 @@ defineSuite([ expect(promise).toBeDefined(); expect(RequestScheduler.statistics.numberOfCancelledRequests).toBe(1); }); + + it('unthrottled requests starve throttled requests', function() { + var deferreds = []; + + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + function createRequest(throttle) { + return new Request({ + url : 'http://foo.com/1', + requestFunction : requestFunction, + throttle : throttle + }); + } + + var throttledRequest = createRequest(true); + RequestScheduler.request(throttledRequest); + + for (var i = 0; i < RequestScheduler.maximumRequests; ++i) { + RequestScheduler.request(createRequest(false)); + } + RequestScheduler.update(); + + expect(throttledRequest.state).toBe(RequestState.ISSUED); + + // Resolve one of the unthrottled requests + deferreds[0].resolve(); + RequestScheduler.update(); + expect(throttledRequest.state).toBe(RequestState.ACTIVE); + }); + + it('request throttled by server is cancelled', function() { + var deferreds = []; + + 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); + }); + + it('debugThrottle', function() { + RequestScheduler.maximumRequests = 0; + + function requestFunction() { + return when.resolve(); + } + + RequestScheduler.debugThrottle = true; + var request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + var promise = RequestScheduler.request(request); + expect(promise).toBeUndefined(); + + RequestScheduler.debugThrottle = false; + request = new Request({ + throttle : true, + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + promise = RequestScheduler.request(request); + expect(promise).toBeDefined(); + + RequestScheduler.debugThrottle = true; + }); + + it('debugShowStatistics', function() { + spyOn(console, 'log'); + RequestScheduler.debugShowStatistics = true; + + var deferreds = []; + + function requestFunction() { + var deferred = when.defer(); + deferreds.push(deferred); + return deferred.promise; + } + + function createRequest() { + return new Request({ + url : 'https://foo.com/1', + requestFunction : requestFunction + }); + } + + var requestToCancel = createRequest(); + RequestScheduler.request(createRequest()); + RequestScheduler.request(createRequest()); + RequestScheduler.request(requestToCancel); + RequestScheduler.update(); + + expect(console.log).toHaveBeenCalledWith('Number of attempted requests: 3'); + expect(console.log).toHaveBeenCalledWith('Number of active requests: 3'); + + 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'); + }); }); From 3d91e190de1d8e2138efd17ee588ff0ec58d9c21 Mon Sep 17 00:00:00 2001 From: Ed Mackey Date: Mon, 5 Jun 2017 10:00:14 -0400 Subject: [PATCH 06/41] Update MIME type for glb files, and fix an eslint warning. --- server.js | 5 ++--- web.config | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 99db4c78ef5..8ce9725e899 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'], 'text/plain' : ['glsl'] }); diff --git a/web.config b/web.config index 78ae51dd8fc..5adcfa77232 100644 --- a/web.config +++ b/web.config @@ -9,9 +9,9 @@ - + - + From 61d3d28e2e6ec1ca67d58c0abf9262cb0bbf1bac Mon Sep 17 00:00:00 2001 From: WilliamKHo Date: Mon, 5 Jun 2017 11:21:44 -0400 Subject: [PATCH 07/41] added bug fix and unit tests --- CHANGES.md | 1 + Source/Scene/Camera.js | 65 +++++++++++++++++++++++++++++++++-------------- Specs/Scene/CameraSpec.js | 38 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 90314d86a88..96d3cce9bad 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Change Log * 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) +* Fixed a bug where `Camera.zoom2D` worked incorrectly when the display height was greater than the display width >>>>>>> master ### 1.34 - 2017-06-01 diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index 5e3180a55f5..d8ec6e63ff7 100644 --- a/Source/Scene/Camera.js +++ b/Source/Scene/Camera.js @@ -1817,30 +1817,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/Specs/Scene/CameraSpec.js b/Specs/Scene/CameraSpec.js index 21403eb8147..c2354087818 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; From 949b670e1cddfd78b912a8c429b25e3a816a8853 Mon Sep 17 00:00:00 2001 From: WilliamKHo Date: Mon, 5 Jun 2017 11:26:16 -0400 Subject: [PATCH 08/41] updated CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 96d3cce9bad..85e2bb4a4fe 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Change Log * 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) -* Fixed a bug where `Camera.zoom2D` worked incorrectly when the display height was greater than the display width +* Fixed a bug where `Camera.zoom2D` worked incorrectly when the display height was greater than the display width [#5421] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5421) >>>>>>> master ### 1.34 - 2017-06-01 From e89b942e3b4250bcd29c5d4643d991aa5fa8536d Mon Sep 17 00:00:00 2001 From: Ed Mackey Date: Mon, 5 Jun 2017 14:30:34 -0400 Subject: [PATCH 09/41] Update default model accept header. --- Source/Scene/Model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 56c96a28e10..6780afde6ec 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -154,8 +154,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_SPEC: Figure out correct mime types (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(); From 08e2702510764cfafd62e49b6b1e9d797cd3260e Mon Sep 17 00:00:00 2001 From: Srinivas Kaza Date: Mon, 5 Jun 2017 15:35:37 -0400 Subject: [PATCH 10/41] Replaces DeveloperError(s) with Check(s) in Ellipsoid --- Source/Core/Ellipsoid.js | 56 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/Source/Core/Ellipsoid.js b/Source/Core/Ellipsoid.js index 6f3eba4bcfc..28b0935765e 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,10 +29,13 @@ define([ z = defaultValue(z, 0.0); //>>includeStart('debug', pragmas.debug); - if (x < 0.0 || y < 0.0 || z < 0.0) { + /*if (x < 0.0 || y < 0.0 || z < 0.0) { throw new DeveloperError('All radii components must be greater than or equal to zero.'); - } + }*/ //>>includeEnd('debug'); + Check.typeOf.number.greaterThanOrEquals('x', x, 0.0); + Check.typeOf.number.greaterThanOrEquals('y', y, 0.0); + Check.typeOf.number.greaterThanOrEquals('z', z, 0.0); ellipsoid._radii = new Cartesian3(x, y, z); @@ -283,13 +288,15 @@ define([ */ Ellipsoid.pack = function(value, array, startingIndex) { //>>includeStart('debug', pragmas.debug); - if (!defined(value)) { + /*if (!defined(value)) { throw new DeveloperError('value is required'); } if (!defined(array)) { throw new DeveloperError('array is required'); - } + }*/ //>>includeEnd('debug'); + Check.defined('value', value); + Check.defined('array', array); startingIndex = defaultValue(startingIndex, 0); @@ -308,10 +315,11 @@ define([ */ Ellipsoid.unpack = function(array, startingIndex, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(array)) { + /*if (!defined(array)) { throw new DeveloperError('array is required'); - } + }*/ //>>includeEnd('debug'); + Check.defined('array', array); startingIndex = defaultValue(startingIndex, 0); @@ -338,10 +346,11 @@ define([ */ Ellipsoid.prototype.geodeticSurfaceNormalCartographic = function(cartographic, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartographic)) { + /*if (!defined(cartographic)) { throw new DeveloperError('cartographic is required.'); - } + }*/ //>>includeEnd('debug'); + Check.defined('cartographic', cartographic); var longitude = cartographic.longitude; var latitude = cartographic.latitude; @@ -422,10 +431,11 @@ define([ */ Ellipsoid.prototype.cartographicArrayToCartesianArray = function(cartographics, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartographics)) { + /*if (!defined(cartographics)) { throw new DeveloperError('cartographics is required.'); - } - //>>includeEnd('debug'); + }*/ + //>>includeEnd('debug') + Check.defined('cartographics', cartographics); var length = cartographics.length; if (!defined(result)) { @@ -496,10 +506,11 @@ define([ */ Ellipsoid.prototype.cartesianArrayToCartographicArray = function(cartesians, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartesians)) { + /*if (!defined(cartesians)) { throw new DeveloperError('cartesians is required.'); - } + }*/ //>>includeEnd('debug'); + Check.defined('cartesians', cartesians); var length = cartesians.length; if (!defined(result)) { @@ -536,10 +547,11 @@ define([ */ Ellipsoid.prototype.scaleToGeocentricSurface = function(cartesian, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(cartesian)) { + /*if (!defined(cartesian)) { throw new DeveloperError('cartesian is required.'); - } + }*/ //>>includeEnd('debug'); + Check.defined('cartesian', cartesian); if (!defined(result)) { result = new Cartesian3(); @@ -633,7 +645,7 @@ define([ */ Ellipsoid.prototype.getSurfaceNormalIntersectionWithZAxis = function(position, buffer, result) { //>>includeStart('debug', pragmas.debug); - if (!defined(position)) { + /*if (!defined(position)) { throw new DeveloperError('position is required.'); } if (!CesiumMath.equalsEpsilon(this._radii.x, this._radii.y, CesiumMath.EPSILON15)) { @@ -641,9 +653,19 @@ define([ } if (this._radii.z === 0) { throw new DeveloperError('Ellipsoid.radii.z must be greater than 0'); - } + }*/ //>>includeEnd('debug'); + Check.defined('position', position); + + // While it would be more idiomatic to use a Check.typeOf.number.something here, + // the resulting error message is a lot harder to read. + 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)'); + } + + Check.typeOf.number.greaterThan('_radii.z', this._radii.z, 0); + buffer = defaultValue(buffer, 0.0); var sqauredXOverSquaredZ = this._sqauredXOverSquaredZ; From a6d90adc47228eb093cfe9d319f01ea45dd8cccb Mon Sep 17 00:00:00 2001 From: Srinivas Kaza Date: Mon, 5 Jun 2017 17:08:02 -0400 Subject: [PATCH 11/41] Fixes similar to the ones in Color --- Source/Core/Ellipsoid.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Source/Core/Ellipsoid.js b/Source/Core/Ellipsoid.js index 28b0935765e..816b61fe1b7 100644 --- a/Source/Core/Ellipsoid.js +++ b/Source/Core/Ellipsoid.js @@ -29,13 +29,10 @@ 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.'); - }*/ - //>>includeEnd('debug'); 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); @@ -294,9 +291,11 @@ define([ if (!defined(array)) { throw new DeveloperError('array is required'); }*/ - //>>includeEnd('debug'); - Check.defined('value', value); + + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('value', value); Check.defined('array', array); + //>>includeEnd('debug'); startingIndex = defaultValue(startingIndex, 0); @@ -350,7 +349,7 @@ define([ throw new DeveloperError('cartographic is required.'); }*/ //>>includeEnd('debug'); - Check.defined('cartographic', cartographic); + Check.typeOf.object('cartographic', cartographic); var longitude = cartographic.longitude; var latitude = cartographic.latitude; @@ -658,8 +657,6 @@ define([ Check.defined('position', position); - // While it would be more idiomatic to use a Check.typeOf.number.something here, - // the resulting error message is a lot harder to read. 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)'); } From 8d698e960dfd3ac7186ad94af1e464a58ea504d4 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 5 Jun 2017 21:12:37 -0400 Subject: [PATCH 12/41] Fix GEE --- .../Core/GoogleEarthEnterpriseTerrainProvider.js | 40 ++++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js index 6ba06b43df9..5ed226e6c56 100644 --- a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js +++ b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js @@ -17,6 +17,7 @@ define([ './Math', './Rectangle', './Request', + './RequestState', './RequestType', './RuntimeError', './TaskProcessor', @@ -39,6 +40,7 @@ define([ CesiumMath, Rectangle, Request, + RequestState, RequestType, RuntimeError, TaskProcessor, @@ -156,6 +158,7 @@ define([ this._terrainCache = new TerrainCache(); this._terrainPromises = {}; + this._terrainRequests = {}; this._errorEvent = new Event(); @@ -436,10 +439,13 @@ 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 if (typeof request === 'boolean') { deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.36.'); @@ -450,13 +456,14 @@ define([ }); } - var requestPromise = loadArrayBuffer(url, undefined, request); + sharedRequest = request; + var requestPromise = loadArrayBuffer(url, undefined, sharedRequest); if (!defined(requestPromise)) { return undefined; // Throttled } - promise = requestPromise + sharedPromise = requestPromise .then(function(terrain) { if (defined(terrain)) { return taskProcessor.scheduleTask({ @@ -488,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)) { @@ -515,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); }); }; From 28c03c13b1b8f9ca5dcae32129fd3eed8ecd0d10 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 5 Jun 2017 22:33:58 -0400 Subject: [PATCH 13/41] Spec fixes --- Source/Core/RequestScheduler.js | 22 ++++++++++------------ Specs/Core/RequestSchedulerSpec.js | 10 ++++++++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 13301e4b474..fbcdde9bbfc 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -116,11 +116,10 @@ define([ // 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 (priorityHeapLength.length > value) { - var request = priorityHeapLength.pop(); - request.cancel(); + while (requestHeap.length > value) { + var request = requestHeap.pop(); + cancelRequest(request); } - RequestScheduler.update(); } priorityHeapLength = value; requestHeap.maximumLength = value; @@ -161,17 +160,16 @@ define([ * @private */ RequestScheduler.clearForSpecs = function() { - var i; var length = activeRequests.length; - for (i = 0; i < length; ++i) { - activeRequests[i].cancel(); - } - length = requestHeap.length; - for (i = 0; i < length; ++i) { - requestHeap.internalArray[i].cancel(); + for (var i = 0; i < length; ++i) { + cancelRequest(activeRequests[i]); } + activeRequests.length = 0; - RequestScheduler.update(); + while (requestHeap.length > 0) { + var request = requestHeap.pop(); + cancelRequest(request); + } // Clear stats statistics.numberOfAttemptedRequests = 0; diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index c4b4b5005b1..67ba30fa23a 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -15,15 +15,18 @@ defineSuite([ var originalMaximumRequests; var originalMaximumRequestsPerServer; + var originalPriorityHeapLength; - beforeEach(function() { + beforeAll(function() { originalMaximumRequests = RequestScheduler.maximumRequests; originalMaximumRequestsPerServer = RequestScheduler.maximumRequestsPerServer; + originalPriorityHeapLength = RequestScheduler.priorityHeapLength; }); afterEach(function() { RequestScheduler.maximumRequests = originalMaximumRequests; RequestScheduler.maximumRequestsPerServer = originalMaximumRequestsPerServer; + RequestScheduler.priorityHeapLength = originalPriorityHeapLength; }); it('request throws when request is undefined', function() { @@ -219,8 +222,11 @@ defineSuite([ expect(promise).toBeUndefined(); RequestScheduler.priorityHeapLength = 3; - promise = RequestScheduler.request(createRequest(1.0)); + 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; From 38b9d5530d797e9e3bf25536f5dca21aa1efd481 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 5 Jun 2017 23:25:29 -0400 Subject: [PATCH 14/41] Fixed tests by letting unfinished tests go through --- Source/Core/RequestScheduler.js | 30 +++++++++++++++++++----------- Source/Core/RequestState.js | 3 ++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index fbcdde9bbfc..194529a1a3a 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -160,16 +160,16 @@ define([ * @private */ RequestScheduler.clearForSpecs = function() { + while (requestHeap.length > 0) { + var request = requestHeap.pop(); + startRequest(request); + } var length = activeRequests.length; for (var i = 0; i < length; ++i) { - cancelRequest(activeRequests[i]); + activeRequests[i].state = RequestState.IGNORED; } activeRequests.length = 0; - - while (requestHeap.length > 0) { - var request = requestHeap.pop(); - cancelRequest(request); - } + numberOfActiveRequestsByServer = {}; // Clear stats statistics.numberOfAttemptedRequests = 0; @@ -206,8 +206,12 @@ define([ if (request.state === RequestState.CANCELLED) { return; } - --statistics.numberOfActiveRequests; - --numberOfActiveRequestsByServer[request.server]; + + if (request.state !== RequestState.IGNORED) { + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.server]; + } + request.state = RequestState.RECEIVED; request.deferred.resolve(results); }; @@ -218,9 +222,13 @@ define([ if (request.state === RequestState.CANCELLED) { return; } - ++statistics.numberOfFailedRequests; - --statistics.numberOfActiveRequests; - --numberOfActiveRequestsByServer[request.server]; + + if (request.state !== RequestState.IGNORED) { + ++statistics.numberOfFailedRequests; + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.server]; + } + request.state = RequestState.FAILED; request.deferred.reject(error); }; diff --git a/Source/Core/RequestState.js b/Source/Core/RequestState.js index 77aea959ffb..7cd28c4e65d 100644 --- a/Source/Core/RequestState.js +++ b/Source/Core/RequestState.js @@ -14,7 +14,8 @@ define([ ACTIVE : 2, // Actual http request has been sent. RECEIVED : 3, // Request completed successfully. CANCELLED : 4, // Request was cancelled, either explicitly or automatically because of low priority. - FAILED : 5 // Request failed. + FAILED : 5, // Request failed. + IGNORED : 6 // For RequestScheduler.clearForSpecs - lets requests finish but doesn't contribute to statistics. }; return freezeObject(RequestState); From f32928f1a85ae2b0ba8f2c21bd40e47f7e60e674 Mon Sep 17 00:00:00 2001 From: Srinivas Kaza Date: Tue, 6 Jun 2017 10:08:01 -0400 Subject: [PATCH 15/41] Checks are now in debug areas; commented code removed --- Source/Core/Ellipsoid.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Source/Core/Ellipsoid.js b/Source/Core/Ellipsoid.js index 816b61fe1b7..be807efd163 100644 --- a/Source/Core/Ellipsoid.js +++ b/Source/Core/Ellipsoid.js @@ -644,24 +644,14 @@ define([ */ Ellipsoid.prototype.getSurfaceNormalIntersectionWithZAxis = function(position, buffer, result) { //>>includeStart('debug', pragmas.debug); - /*if (!defined(position)) { - throw new DeveloperError('position is required.'); - } - 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'); - }*/ - //>>includeEnd('debug'); - Check.defined('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)'); } - Check.typeOf.number.greaterThan('_radii.z', this._radii.z, 0); + Check.typeOf.number.greaterThan('Ellipsoid.radii.z', this._radii.z, 0); + //>>includeEnd('debug'); buffer = defaultValue(buffer, 0.0); From d4a7b8f0ef6304b4f1b0d2f549dd1185c7d888e3 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Tue, 6 Jun 2017 10:15:03 -0400 Subject: [PATCH 16/41] Use shared config package for ESLint. Rename .eslintrc -> .eslintrc.json. --- .eslintrc | 40 ---------------------------------------- .eslintrc.json | 3 +++ Apps/Sandcastle/.eslintrc | 13 ------------- Apps/Sandcastle/.eslintrc.json | 3 +++ Specs/.eslintrc | 6 ------ Specs/.eslintrc.json | 3 +++ package.json | 1 + 7 files changed, 10 insertions(+), 59 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.json delete mode 100644 Apps/Sandcastle/.eslintrc create mode 100644 Apps/Sandcastle/.eslintrc.json delete mode 100644 Specs/.eslintrc create mode 100644 Specs/.eslintrc.json 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..b98cdb40634 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "cesium" +} diff --git a/Apps/Sandcastle/.eslintrc b/Apps/Sandcastle/.eslintrc deleted file mode 100644 index 2fb0cba37fb..00000000000 --- a/Apps/Sandcastle/.eslintrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../.eslintrc", - "globals": { - "JSON": true, - "require": true, - "console": true, - "Sandcastle": true, - "Cesium": true - }, - "rules": { - "no-unused-vars": ["off"] - } -} diff --git a/Apps/Sandcastle/.eslintrc.json b/Apps/Sandcastle/.eslintrc.json new file mode 100644 index 00000000000..88528b77f41 --- /dev/null +++ b/Apps/Sandcastle/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "cesium/browser" +} diff --git a/Specs/.eslintrc b/Specs/.eslintrc deleted file mode 100644 index c9b69aa5b6b..00000000000 --- a/Specs/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../.eslintrc", - "env": { - "jasmine": true - } -} diff --git a/Specs/.eslintrc.json b/Specs/.eslintrc.json new file mode 100644 index 00000000000..1216df42491 --- /dev/null +++ b/Specs/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "cesium/browser-test" +} diff --git a/package.json b/package.json index ac2988c0a2a..1b8d6cb9ac9 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "compressible": "^2.0.9", "compression": "^1.6.2", "electron": "^1.6.1", + "eslint-config-cesium": "^1.0.0", "eslint-plugin-html": "^2.0.3", "event-stream": "^3.3.4", "express": "^4.15.0", From a3e0e91411e7eceee607d769ce378e04590066e1 Mon Sep 17 00:00:00 2001 From: Ed Mackey Date: Tue, 6 Jun 2017 10:38:33 -0400 Subject: [PATCH 17/41] Tweak comment, and update CHANGES.md --- CHANGES.md | 3 ++- Source/Scene/Model.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8eb64f656a9..0fc81b7df66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,11 @@ Change Log ### 1.35 - 2017-07-05 * Deprecated - * `GoogleEarthImageryProvider` has been deprecated and will be removed in Cesium 1.37, use `GoogleEarthEnterpriseMapsProvider` instead. + * `GoogleEarthImageryProvider` has been deprecated and will be removed in Cesium 1.37, use `GoogleEarthEnterpriseMapsProvider` instead. * 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) * Added Sandcastle demo for ArcticDEM data. [#5224](https://github.com/AnalyticalGraphicsInc/cesium/issues/5224) ### 1.34 - 2017-06-01 diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 6780afde6ec..065408ca19f 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -154,7 +154,7 @@ define([ FAILED : 3 }; - // GLTF_SPEC: Figure out correct mime types (https://github.com/KhronosGroup/glTF/issues/412 and https://github.com/KhronosGroup/glTF/issues/943) + // 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() { From 7fb68cb458931b8c1738caec0053c0af22f771ef Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Tue, 6 Jun 2017 10:49:21 -0400 Subject: [PATCH 18/41] Create Node package for ESLint shareable config. --- Tools/eslint-config-cesium/.eslintrc.json | 3 +++ Tools/eslint-config-cesium/.npmignore | 0 Tools/eslint-config-cesium/CHANGES.md | 6 +++++ Tools/eslint-config-cesium/LICENSE.md | 7 ++++++ Tools/eslint-config-cesium/README.md | 1 + Tools/eslint-config-cesium/browser-test.js | 6 +++++ Tools/eslint-config-cesium/browser.js | 13 ++++++++++ Tools/eslint-config-cesium/index.js | 40 ++++++++++++++++++++++++++++++ Tools/eslint-config-cesium/node-test.js | 0 Tools/eslint-config-cesium/node.js | 9 +++++++ Tools/eslint-config-cesium/package.json | 17 +++++++++++++ package.json | 2 +- 12 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 Tools/eslint-config-cesium/.eslintrc.json create mode 100644 Tools/eslint-config-cesium/.npmignore create mode 100644 Tools/eslint-config-cesium/CHANGES.md create mode 100644 Tools/eslint-config-cesium/LICENSE.md create mode 100644 Tools/eslint-config-cesium/README.md create mode 100644 Tools/eslint-config-cesium/browser-test.js create mode 100644 Tools/eslint-config-cesium/browser.js create mode 100644 Tools/eslint-config-cesium/index.js create mode 100644 Tools/eslint-config-cesium/node-test.js create mode 100644 Tools/eslint-config-cesium/node.js create mode 100644 Tools/eslint-config-cesium/package.json diff --git a/Tools/eslint-config-cesium/.eslintrc.json b/Tools/eslint-config-cesium/.eslintrc.json new file mode 100644 index 00000000000..9ef3669f573 --- /dev/null +++ b/Tools/eslint-config-cesium/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends" : "cesium/node" +} diff --git a/Tools/eslint-config-cesium/.npmignore b/Tools/eslint-config-cesium/.npmignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Tools/eslint-config-cesium/CHANGES.md b/Tools/eslint-config-cesium/CHANGES.md new file mode 100644 index 00000000000..e0d3d1417c9 --- /dev/null +++ b/Tools/eslint-config-cesium/CHANGES.md @@ -0,0 +1,6 @@ +Change Log +========== + +### 0.1.0 - 2017-06-06 + +* Initial release. diff --git a/Tools/eslint-config-cesium/LICENSE.md b/Tools/eslint-config-cesium/LICENSE.md new file mode 100644 index 00000000000..e911f68692e --- /dev/null +++ b/Tools/eslint-config-cesium/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2017 Ottavio Hartman, Analytical Graphics, Inc., and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/Tools/eslint-config-cesium/README.md b/Tools/eslint-config-cesium/README.md new file mode 100644 index 00000000000..93019f9e8f9 --- /dev/null +++ b/Tools/eslint-config-cesium/README.md @@ -0,0 +1 @@ +A [shareable ESLint config](http://eslint.org/docs/developer-guide/shareable-configs) for [Cesium](https://cesiumjs.org/). diff --git a/Tools/eslint-config-cesium/browser-test.js b/Tools/eslint-config-cesium/browser-test.js new file mode 100644 index 00000000000..4a71150a4e6 --- /dev/null +++ b/Tools/eslint-config-cesium/browser-test.js @@ -0,0 +1,6 @@ +module.exports = { + "extends": "cesium", + "env": { + "jasmine": true + } +}; diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js new file mode 100644 index 00000000000..d188d4d6d54 --- /dev/null +++ b/Tools/eslint-config-cesium/browser.js @@ -0,0 +1,13 @@ +module.exports = { + "extends": "eslint-config-cesium", + "globals": { + "JSON": true, + "require": true, + "console": true, + "Sandcastle": true, + "Cesium": true + }, + "rules": { + "no-unused-vars": ["off"] + } +}; diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js new file mode 100644 index 00000000000..9a1dd95d3e8 --- /dev/null +++ b/Tools/eslint-config-cesium/index.js @@ -0,0 +1,40 @@ +module.exports = { + "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/Tools/eslint-config-cesium/node-test.js b/Tools/eslint-config-cesium/node-test.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js new file mode 100644 index 00000000000..632862ea8bb --- /dev/null +++ b/Tools/eslint-config-cesium/node.js @@ -0,0 +1,9 @@ +module.exports = { + "extends": "cesium", + "env": { + "node": true + }, + "rules": { + "strict": ["off"] + } +}; diff --git a/Tools/eslint-config-cesium/package.json b/Tools/eslint-config-cesium/package.json new file mode 100644 index 00000000000..e3b6f547c9e --- /dev/null +++ b/Tools/eslint-config-cesium/package.json @@ -0,0 +1,17 @@ +{ + "name": "eslint-config-cesium", + "version": "0.1.0", + "description": "ESLint shareable configs for Cesium", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "eslint" + ], + "author": "Ottavio Hartman (https://github.com/omh1280)", + "license": "Apache-2.0", + "peerDependencies": { + "eslint": ">= 3" + } +} diff --git a/package.json b/package.json index 1b8d6cb9ac9..f4cc29a11dc 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "compressible": "^2.0.9", "compression": "^1.6.2", "electron": "^1.6.1", - "eslint-config-cesium": "^1.0.0", + "eslint-config-cesium": "^0.1.0", "eslint-plugin-html": "^2.0.3", "event-stream": "^3.3.4", "express": "^4.15.0", From be2717c709e31fc328334bc5cc1cc913d5385321 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Tue, 6 Jun 2017 11:41:14 -0400 Subject: [PATCH 19/41] Deleted LICENSE.md --- Tools/eslint-config-cesium/LICENSE.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 Tools/eslint-config-cesium/LICENSE.md diff --git a/Tools/eslint-config-cesium/LICENSE.md b/Tools/eslint-config-cesium/LICENSE.md deleted file mode 100644 index e911f68692e..00000000000 --- a/Tools/eslint-config-cesium/LICENSE.md +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2017 Ottavio Hartman, Analytical Graphics, Inc., and Contributors - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. From da3420d870570d811e73f27b551c3cc64f4a5019 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 6 Jun 2017 12:04:21 -0400 Subject: [PATCH 20/41] Remove RequestScheduler.clearForSpecs from being called for every test --- Source/Core/RequestScheduler.js | 4 +-- Specs/Core/CesiumTerrainProviderSpec.js | 4 +++ .../GoogleEarthEnterpriseTerrainProviderSpec.js | 4 +++ Specs/Core/RequestSchedulerSpec.js | 34 ++++++++++++++++++++++ Specs/Core/VRTheWorldTerrainProviderSpec.js | 1 + Specs/customizeJasmine.js | 4 +-- Specs/karma-main.js | 4 +-- Specs/spec-main.js | 2 -- 8 files changed, 47 insertions(+), 10 deletions(-) diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 194529a1a3a..2c302924d78 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -162,11 +162,11 @@ define([ RequestScheduler.clearForSpecs = function() { while (requestHeap.length > 0) { var request = requestHeap.pop(); - startRequest(request); + cancelRequest(request); } var length = activeRequests.length; for (var i = 0; i < length; ++i) { - activeRequests[i].state = RequestState.IGNORED; + cancelRequest(activeRequests[i]); } activeRequests.length = 0; numberOfActiveRequestsByServer = {}; diff --git a/Specs/Core/CesiumTerrainProviderSpec.js b/Specs/Core/CesiumTerrainProviderSpec.js index 27bc94a4730..c15e0097792 100644 --- a/Specs/Core/CesiumTerrainProviderSpec.js +++ b/Specs/Core/CesiumTerrainProviderSpec.js @@ -31,6 +31,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadWithXhr.load = loadWithXhr.defaultLoad; }); diff --git a/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js b/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js index 024d4a23e34..9917d46c18b 100644 --- a/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js +++ b/Specs/Core/GoogleEarthEnterpriseTerrainProviderSpec.js @@ -84,6 +84,10 @@ defineSuite([ }); } + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadWithXhr.load = loadWithXhr.defaultLoad; }); diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index 67ba30fa23a..63a7e1a90aa 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -23,6 +23,10 @@ defineSuite([ originalPriorityHeapLength = RequestScheduler.priorityHeapLength; }); + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { RequestScheduler.maximumRequests = originalMaximumRequests; RequestScheduler.maximumRequestsPerServer = originalMaximumRequestsPerServer; @@ -128,6 +132,11 @@ defineSuite([ expect(statistics.numberOfActiveRequests).toBe(3); expect(promise6).toBeDefined(); + + var length = deferreds.length; + for (var i = 0; i < length; ++i) { + deferreds[i].resolve(); + } }); it('honors maximumRequestsPerServer', function() { @@ -191,6 +200,11 @@ defineSuite([ expect(RequestScheduler.numberOfActiveRequestsByServer(server)).toBe(3); expect(promise6).toBeDefined(); + + var length = deferreds.length; + for (var i = 0; i < length; ++i) { + deferreds[i].resolve(); + } }); it('honors priorityHeapLength', function() { @@ -231,6 +245,11 @@ defineSuite([ // 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(); + } }); function testImmediateRequest(url, dataOrBlobUri) { @@ -565,6 +584,11 @@ defineSuite([ deferreds[0].resolve(); RequestScheduler.update(); expect(throttledRequest.state).toBe(RequestState.ACTIVE); + + var length = deferreds.length; + for (var j = 0; j < length; ++j) { + deferreds[j].resolve(); + } }); it('request throttled by server is cancelled', function() { @@ -594,6 +618,11 @@ defineSuite([ RequestScheduler.update(); expect(throttledRequest.state).toBe(RequestState.CANCELLED); + + var length = deferreds.length; + for (var j = 0; j < length; ++j) { + deferreds[j].resolve(); + } }); it('debugThrottle', function() { @@ -659,5 +688,10 @@ defineSuite([ 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(); + } }); }); diff --git a/Specs/Core/VRTheWorldTerrainProviderSpec.js b/Specs/Core/VRTheWorldTerrainProviderSpec.js index 205d7c317b6..cf5858abc29 100644 --- a/Specs/Core/VRTheWorldTerrainProviderSpec.js +++ b/Specs/Core/VRTheWorldTerrainProviderSpec.js @@ -28,6 +28,7 @@ defineSuite([ 'use strict'; beforeEach(function() { + RequestScheduler.clearForSpecs(); loadWithXhr.load = function(url, responseType, method, data, headers, deferred, overrideMimeType) { setTimeout(function() { var parser = new DOMParser(); diff --git a/Specs/customizeJasmine.js b/Specs/customizeJasmine.js index e999849c95a..e857b01912a 100644 --- a/Specs/customizeJasmine.js +++ b/Specs/customizeJasmine.js @@ -9,7 +9,7 @@ define([ equalsMethodEqualityTester) { "use strict"; - return function (env, includedCategory, excludedCategory, webglValidation, webglStub, release, RequestScheduler) { + return function (env, includedCategory, excludedCategory, webglValidation, webglStub, release) { function defineSuite(deps, name, suite, categories, focus) { /*global define,describe,fdescribe*/ if (typeof suite === 'object' || typeof suite === 'string') { @@ -72,8 +72,6 @@ define([ window.it = function(description, f, timeout, categories) { originalIt(description, function(done) { - // Clear the RequestScheduler before for each test in case requests are still active from previous tests - RequestScheduler.clearForSpecs(); var result = f(); when(result, function() { done(); diff --git a/Specs/karma-main.js b/Specs/karma-main.js index e7fc78ff3cc..46b04fc0451 100644 --- a/Specs/karma-main.js +++ b/Specs/karma-main.js @@ -53,12 +53,10 @@ } require([ - 'Core/RequestScheduler', 'Specs/customizeJasmine' ], function( - RequestScheduler, customizeJasmine) { - customizeJasmine(jasmine.getEnv(), included, excluded, webglValidation, webglStub, release, RequestScheduler); + customizeJasmine(jasmine.getEnv(), included, excluded, webglValidation, webglStub, release); var specFiles = Object.keys(__karma__.files).filter(function(file) { return /Spec\.js$/.test(file); diff --git a/Specs/spec-main.js b/Specs/spec-main.js index fb4e707f5ba..9ba3c174eee 100644 --- a/Specs/spec-main.js +++ b/Specs/spec-main.js @@ -136,8 +136,6 @@ window.it = function(description, f, timeout, categories) { originalIt(description, function(done) { - // Clear the RequestScheduler before for each test in case requests are still active from previous tests - Cesium.RequestScheduler.clearForSpecs(); var result = f(); when(result, function() { done(); From 1e261d93a6151fd7211e24948ba5e1f0dff947c4 Mon Sep 17 00:00:00 2001 From: Rachel Hwang Date: Wed, 7 Jun 2017 12:01:37 -0400 Subject: [PATCH 21/41] edits, adding thumbnail --- Apps/Sandcastle/gallery/Callback Property.html | 58 ++++++++++++++++--------- Apps/Sandcastle/gallery/Callback Property.jpg | Bin 0 -> 58311 bytes 2 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 Apps/Sandcastle/gallery/Callback Property.jpg diff --git a/Apps/Sandcastle/gallery/Callback Property.html b/Apps/Sandcastle/gallery/Callback Property.html index d288b13b1dd..e4c00147d16 100644 --- a/Apps/Sandcastle/gallery/Callback Property.html +++ b/Apps/Sandcastle/gallery/Callback Property.html @@ -29,52 +29,70 @@ //Sandcastle_Begin // This example illustrates a Callback Property, a property whose // value is lazily evaluated by a callback function. +// Use a CallbackProperty when your data can't be pre-computed +// or needs to be derived from other properties at runtime. var viewer = new Cesium.Viewer('cesiumContainer'); -var startLat = 35; -var startLon = -120; -var endLon = startLon; - -// Update the polyline endpoints to animate the line length. -function getPositions() { - endLon += 0.01; - return Cesium.Cartesian3.fromDegreesArray([startLon, startLat, endLon, startLat]); -} +var startLatitude = 35; +var startLongitude = -120; +var endLongitude; +var startTime = Cesium.JulianDate.now(); // Add a polyline to the scene. Positions are dynamic. +var isConstant = false; var redLine = viewer.entities.add({ polyline : { // This callback updates positions each frame. - positions : new Cesium.CallbackProperty(getPositions, false), + positions : new Cesium.CallbackProperty(function(time, result) { + endLongitude = startLongitude + 0.001 * Cesium.JulianDate.secondsDifference(time, startTime); + return Cesium.Cartesian3.fromDegreesArray([startLongitude, startLatitude, endLongitude, startLatitude], Cesium.Ellipsoid.WGS84, result); + }, isConstant), width : 5, material : Cesium.Color.RED } }); -var startCarto = Cesium.Cartographic.fromDegrees(startLon, startLat); -var endCarto = new Cesium.Cartographic(); // use scratch object to avoid new allocations per frame. +var startCartographic = Cesium.Cartographic.fromDegrees(startLongitude, startLatitude); + +// use scratch object to avoid new allocations per frame. +var endCartographic = new Cesium.Cartographic(); +var scratch = new Cesium.Cartographic(); var geodesic = new Cesium.EllipsoidGeodesic(); // Calculate the length of the line -function getLength() { +function getLength(time, result) { // Get the end position from the polyLine's callback. - var endPoint = redLine.polyline.positions.getValue()[1]; - endCarto = Cesium.Cartographic.fromCartesian(endPoint); - - geodesic.setEndPoints(startCarto, endCarto); - return 'Length: ' + Math.round(geodesic.surfaceDistance); + var endPoint = redLine.polyline.positions.getValue(time, result)[1]; + endCartographic = Cesium.Cartographic.fromCartesian(endPoint); + + geodesic.setEndPoints(startCartographic, endCartographic); + var lengthInMeters = Math.round(geodesic.surfaceDistance); + return (lengthInMeters / 1000).toFixed(1) + " km"; +} + +function getMidpoint(time, result) { + // Get the end position from the polyLine's callback. + var endPoint = redLine.polyline.positions.getValue(time, result)[1]; + endCartographic = Cesium.Cartographic.fromCartesian(endPoint); + + geodesic.setEndPoints(startCartographic, endCartographic); + var midpointCartographic = geodesic.interpolateUsingFraction(0.5, scratch); + return Cesium.Cartesian3.fromRadians(midpointCartographic.longitude, midpointCartographic.latitude); } // Label the polyline with calculated length. var label = viewer.entities.add({ - position : Cesium.Cartesian3.fromDegrees(startLon, startLat), + position : new Cesium.CallbackProperty(getMidpoint, isConstant), label : { // This callback updates the length to print each frame. - text: new Cesium.CallbackProperty(getLength, false), + text: new Cesium.CallbackProperty(getLength, isConstant), font : '20px sans-serif', pixelOffset : new Cesium.Cartesian2(0.0, 20) } }); + +// Keep the view centered. +viewer.trackedEntity = label; //Sandcastle_End Sandcastle.finishedLoading(); } diff --git a/Apps/Sandcastle/gallery/Callback Property.jpg b/Apps/Sandcastle/gallery/Callback Property.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3a5cf438d74b6fc033ae498ecd8b4bfd59f9f612 GIT binary patch literal 58311 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 lHp-$<2>eLe{&9z{w^ID>=>US~Qwp)%nD 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 O83)Nh*vggv#{Lgyp_dQ< literal 0 HcmV?d00001 From b5724877573152e4ffb531446f033470828eab89 Mon Sep 17 00:00:00 2001 From: Srinivas Kaza Date: Wed, 7 Jun 2017 13:39:54 -0400 Subject: [PATCH 22/41] Deletes more commented code --- Source/Core/Ellipsoid.js | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/Source/Core/Ellipsoid.js b/Source/Core/Ellipsoid.js index be807efd163..36001049ba6 100644 --- a/Source/Core/Ellipsoid.js +++ b/Source/Core/Ellipsoid.js @@ -314,11 +314,8 @@ define([ */ Ellipsoid.unpack = function(array, startingIndex, result) { //>>includeStart('debug', pragmas.debug); - /*if (!defined(array)) { - throw new DeveloperError('array is required'); - }*/ - //>>includeEnd('debug'); Check.defined('array', array); + //>>includeEnd('debug'); startingIndex = defaultValue(startingIndex, 0); @@ -345,11 +342,8 @@ define([ */ Ellipsoid.prototype.geodeticSurfaceNormalCartographic = function(cartographic, result) { //>>includeStart('debug', pragmas.debug); - /*if (!defined(cartographic)) { - throw new DeveloperError('cartographic is required.'); - }*/ - //>>includeEnd('debug'); Check.typeOf.object('cartographic', cartographic); + //>>includeEnd('debug'); var longitude = cartographic.longitude; var latitude = cartographic.latitude; @@ -430,11 +424,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)) { @@ -505,11 +496,8 @@ define([ */ Ellipsoid.prototype.cartesianArrayToCartographicArray = function(cartesians, result) { //>>includeStart('debug', pragmas.debug); - /*if (!defined(cartesians)) { - throw new DeveloperError('cartesians is required.'); - }*/ - //>>includeEnd('debug'); Check.defined('cartesians', cartesians); + //>>includeEnd('debug'); var length = cartesians.length; if (!defined(result)) { @@ -546,11 +534,8 @@ define([ */ Ellipsoid.prototype.scaleToGeocentricSurface = function(cartesian, result) { //>>includeStart('debug', pragmas.debug); - /*if (!defined(cartesian)) { - throw new DeveloperError('cartesian is required.'); - }*/ - //>>includeEnd('debug'); Check.defined('cartesian', cartesian); + //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); From e676d93c83ad042001950dd59e4f40239be9c0f4 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Wed, 7 Jun 2017 13:40:31 -0400 Subject: [PATCH 23/41] Add --- .idea/jsLibraryMappings.xml | 4 ++-- Tools/eslint-config-cesium/.eslintrc.json | 3 --- Tools/eslint-config-cesium/browser-test.js | 5 ++++- Tools/eslint-config-cesium/browser.js | 6 ++++++ Tools/eslint-config-cesium/index.js | 6 +++--- Tools/eslint-config-cesium/node-test.js | 9 +++++++++ Tools/eslint-config-cesium/node.js | 5 ++++- 7 files changed, 28 insertions(+), 10 deletions(-) delete mode 100644 Tools/eslint-config-cesium/.eslintrc.json 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/Tools/eslint-config-cesium/.eslintrc.json b/Tools/eslint-config-cesium/.eslintrc.json deleted file mode 100644 index 9ef3669f573..00000000000 --- a/Tools/eslint-config-cesium/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends" : "cesium/node" -} diff --git a/Tools/eslint-config-cesium/browser-test.js b/Tools/eslint-config-cesium/browser-test.js index 4a71150a4e6..52525700106 100644 --- a/Tools/eslint-config-cesium/browser-test.js +++ b/Tools/eslint-config-cesium/browser-test.js @@ -1,5 +1,8 @@ +/*eslint-env node*/ +'use strict'; + module.exports = { - "extends": "cesium", + "extends": "cesium/browser", "env": { "jasmine": true } diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js index d188d4d6d54..612f1ce3ef4 100644 --- a/Tools/eslint-config-cesium/browser.js +++ b/Tools/eslint-config-cesium/browser.js @@ -1,3 +1,6 @@ +/*eslint-env node*/ +'use strict'; + module.exports = { "extends": "eslint-config-cesium", "globals": { @@ -7,6 +10,9 @@ module.exports = { "Sandcastle": true, "Cesium": true }, + "plugins": [ + "html" + ], "rules": { "no-unused-vars": ["off"] } diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js index 9a1dd95d3e8..24973379399 100644 --- a/Tools/eslint-config-cesium/index.js +++ b/Tools/eslint-config-cesium/index.js @@ -1,3 +1,6 @@ +/*eslint-env node*/ +'use strict'; + module.exports = { "extends": "eslint:recommended", "env": { @@ -16,9 +19,6 @@ module.exports = { "Uint8Array": false, "Uint8ClampedArray": false }, - "plugins": [ - "html" - ], "rules": { "curly": ["error"], "eqeqeq": ["error"], diff --git a/Tools/eslint-config-cesium/node-test.js b/Tools/eslint-config-cesium/node-test.js index e69de29bb2d..384e89a223a 100644 --- a/Tools/eslint-config-cesium/node-test.js +++ b/Tools/eslint-config-cesium/node-test.js @@ -0,0 +1,9 @@ +/*eslint-env node*/ +'use strict'; + +module.exports = { + "extends": "cesium/node", + "env": { + "jasmine": true + } +}; diff --git a/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js index 632862ea8bb..7068bc155c5 100644 --- a/Tools/eslint-config-cesium/node.js +++ b/Tools/eslint-config-cesium/node.js @@ -1,9 +1,12 @@ +/*eslint-env node*/ +'use strict'; + module.exports = { "extends": "cesium", "env": { "node": true }, "rules": { - "strict": ["off"] + "strict": [2, "safe"] } }; From ab5f71c49354fafce3c0cc37b7e009215ed11f70 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Wed, 7 Jun 2017 13:49:52 -0400 Subject: [PATCH 24/41] Change unused-vars rule, update package.json --- .eslintrc.json | 5 ++++- Tools/eslint-config-cesium/.npmignore | 0 Tools/eslint-config-cesium/browser-test.js | 9 --------- Tools/eslint-config-cesium/browser.js | 5 +---- Tools/eslint-config-cesium/node-test.js | 9 --------- Tools/eslint-config-cesium/node.js | 1 + Tools/eslint-config-cesium/package.json | 19 +++++++++++++------ 7 files changed, 19 insertions(+), 29 deletions(-) delete mode 100644 Tools/eslint-config-cesium/.npmignore delete mode 100644 Tools/eslint-config-cesium/browser-test.js delete mode 100644 Tools/eslint-config-cesium/node-test.js diff --git a/.eslintrc.json b/.eslintrc.json index b98cdb40634..26ef3447d12 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "cesium" + "extends": "cesium", + "rules": { + "no-unused-vars": ["off"] + } } diff --git a/Tools/eslint-config-cesium/.npmignore b/Tools/eslint-config-cesium/.npmignore deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/Tools/eslint-config-cesium/browser-test.js b/Tools/eslint-config-cesium/browser-test.js deleted file mode 100644 index 52525700106..00000000000 --- a/Tools/eslint-config-cesium/browser-test.js +++ /dev/null @@ -1,9 +0,0 @@ -/*eslint-env node*/ -'use strict'; - -module.exports = { - "extends": "cesium/browser", - "env": { - "jasmine": true - } -}; diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js index 612f1ce3ef4..12bd2432ee1 100644 --- a/Tools/eslint-config-cesium/browser.js +++ b/Tools/eslint-config-cesium/browser.js @@ -12,8 +12,5 @@ module.exports = { }, "plugins": [ "html" - ], - "rules": { - "no-unused-vars": ["off"] - } + ] }; diff --git a/Tools/eslint-config-cesium/node-test.js b/Tools/eslint-config-cesium/node-test.js deleted file mode 100644 index 384e89a223a..00000000000 --- a/Tools/eslint-config-cesium/node-test.js +++ /dev/null @@ -1,9 +0,0 @@ -/*eslint-env node*/ -'use strict'; - -module.exports = { - "extends": "cesium/node", - "env": { - "jasmine": true - } -}; diff --git a/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js index 7068bc155c5..5b5ca70f5fc 100644 --- a/Tools/eslint-config-cesium/node.js +++ b/Tools/eslint-config-cesium/node.js @@ -4,6 +4,7 @@ module.exports = { "extends": "cesium", "env": { + "browser": false, "node": true }, "rules": { diff --git a/Tools/eslint-config-cesium/package.json b/Tools/eslint-config-cesium/package.json index e3b6f547c9e..4bff5c4aa2b 100644 --- a/Tools/eslint-config-cesium/package.json +++ b/Tools/eslint-config-cesium/package.json @@ -2,15 +2,22 @@ "name": "eslint-config-cesium", "version": "0.1.0", "description": "ESLint shareable configs for Cesium", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "license": "Apache-2.0", + "author": { + "name": "Analytical Graphics, Inc.", + "url": "http://www.agi.com" }, + "contributors": [ + { + "name": "Ottavio Hartman (https://github.com/omh1280)", + "url": "https://github.com/AnalyticalGraphicsInc/cesium/blob/master/CONTRIBUTORS.md" + } + ], "keywords": [ - "eslint" + "eslint", + "config", + "cesium" ], - "author": "Ottavio Hartman (https://github.com/omh1280)", - "license": "Apache-2.0", "peerDependencies": { "eslint": ">= 3" } From 41df3e23958f8bd60a7fbb711258f400e4ba7597 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Wed, 7 Jun 2017 14:00:41 -0400 Subject: [PATCH 25/41] Add jasmine env and turn off no-unused-vars for Sandcastle --- Apps/Sandcastle/.eslintrc.json | 5 ++++- Specs/.eslintrc.json | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Apps/Sandcastle/.eslintrc.json b/Apps/Sandcastle/.eslintrc.json index 88528b77f41..871d204e753 100644 --- a/Apps/Sandcastle/.eslintrc.json +++ b/Apps/Sandcastle/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "cesium/browser" + "extends": "cesium/browser", + "rules": { + "no-unused-vars": ["off"] + } } diff --git a/Specs/.eslintrc.json b/Specs/.eslintrc.json index 1216df42491..563e9655374 100644 --- a/Specs/.eslintrc.json +++ b/Specs/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "cesium/browser-test" + "extends": "cesium/browser", + "env": { + "jasmine": true + } } From fcd29db6c2c4ce25858b43304939dcb68acff4e8 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Wed, 7 Jun 2017 16:31:40 -0400 Subject: [PATCH 26/41] Clean up .eslintrc files, update package.json, remove redundant configs --- .eslintrc.json | 5 +-- Apps/Sandcastle/.eslintrc.json | 9 ++++- Specs/.eslintrc.json | 2 +- Tools/eslint-config-cesium/.eslintrc.json | 3 ++ Tools/eslint-config-cesium/browser.js | 15 +++----- Tools/eslint-config-cesium/index.js | 64 +++++++++++++++---------------- Tools/eslint-config-cesium/node.js | 11 ++---- Tools/eslint-config-cesium/package.json | 3 +- 8 files changed, 52 insertions(+), 60 deletions(-) create mode 100644 Tools/eslint-config-cesium/.eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json index 26ef3447d12..88528b77f41 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,3 @@ { - "extends": "cesium", - "rules": { - "no-unused-vars": ["off"] - } + "extends": "cesium/browser" } diff --git a/Apps/Sandcastle/.eslintrc.json b/Apps/Sandcastle/.eslintrc.json index 871d204e753..b9795620faa 100644 --- a/Apps/Sandcastle/.eslintrc.json +++ b/Apps/Sandcastle/.eslintrc.json @@ -1,5 +1,12 @@ { - "extends": "cesium/browser", + "extends": "../../.eslintrc.json", + "globals": { + "JSON": true, + "require": true, + "console": true, + "Sandcastle": true, + "Cesium": true + }, "rules": { "no-unused-vars": ["off"] } diff --git a/Specs/.eslintrc.json b/Specs/.eslintrc.json index 563e9655374..297a41b8f96 100644 --- a/Specs/.eslintrc.json +++ b/Specs/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": "cesium/browser", + "extends": "../.eslintrc.json", "env": { "jasmine": true } diff --git a/Tools/eslint-config-cesium/.eslintrc.json b/Tools/eslint-config-cesium/.eslintrc.json new file mode 100644 index 00000000000..fcf4081f55c --- /dev/null +++ b/Tools/eslint-config-cesium/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "cesium/node" +} diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js index 12bd2432ee1..edf8d76136a 100644 --- a/Tools/eslint-config-cesium/browser.js +++ b/Tools/eslint-config-cesium/browser.js @@ -1,16 +1,11 @@ -/*eslint-env node*/ 'use strict'; module.exports = { - "extends": "eslint-config-cesium", - "globals": { - "JSON": true, - "require": true, - "console": true, - "Sandcastle": true, - "Cesium": true + 'extends': 'cesium', + 'env': { + 'browser': true }, - "plugins": [ - "html" + 'plugins': [ + 'html' ] }; diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js index 24973379399..02025f073a7 100644 --- a/Tools/eslint-config-cesium/index.js +++ b/Tools/eslint-config-cesium/index.js @@ -1,40 +1,36 @@ -/*eslint-env node*/ 'use strict'; module.exports = { - "extends": "eslint:recommended", - "env": { - "browser": true + 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 }, - "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": "none"}], - "semi": ["error"], - "strict": ["error"], - "wrap-iife": ["error", "any"] + 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/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js index 5b5ca70f5fc..91eaf70fbdc 100644 --- a/Tools/eslint-config-cesium/node.js +++ b/Tools/eslint-config-cesium/node.js @@ -1,13 +1,8 @@ -/*eslint-env node*/ 'use strict'; module.exports = { - "extends": "cesium", - "env": { - "browser": false, - "node": true - }, - "rules": { - "strict": [2, "safe"] + 'extends': 'cesium', + 'env': { + 'node': true } }; diff --git a/Tools/eslint-config-cesium/package.json b/Tools/eslint-config-cesium/package.json index 4bff5c4aa2b..8c55a82bf7b 100644 --- a/Tools/eslint-config-cesium/package.json +++ b/Tools/eslint-config-cesium/package.json @@ -9,13 +9,12 @@ }, "contributors": [ { - "name": "Ottavio Hartman (https://github.com/omh1280)", + "name": "Cesium community", "url": "https://github.com/AnalyticalGraphicsInc/cesium/blob/master/CONTRIBUTORS.md" } ], "keywords": [ "eslint", - "config", "cesium" ], "peerDependencies": { From b3a403b405cdd396f069dae0232628d58a3d357f Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Wed, 7 Jun 2017 16:34:36 -0400 Subject: [PATCH 27/41] Remove more quotes from named types --- Tools/eslint-config-cesium/browser.js | 8 ++++---- Tools/eslint-config-cesium/index.js | 4 ++-- Tools/eslint-config-cesium/node.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js index edf8d76136a..1b9a8440f35 100644 --- a/Tools/eslint-config-cesium/browser.js +++ b/Tools/eslint-config-cesium/browser.js @@ -1,11 +1,11 @@ 'use strict'; module.exports = { - 'extends': 'cesium', - 'env': { - 'browser': true + extends: 'cesium', + env: { + browser: true }, - 'plugins': [ + plugins: [ 'html' ] }; diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js index 02025f073a7..ee6e5943c71 100644 --- a/Tools/eslint-config-cesium/index.js +++ b/Tools/eslint-config-cesium/index.js @@ -19,7 +19,7 @@ module.exports = { curly: ['error'], eqeqeq: ['error'], 'guard-for-in': ['error'], - 'new-cap': ['error', {'properties': false}], + 'new-cap': ['error', {properties: false}], 'no-caller': ['error'], 'no-console': 'off', 'no-empty': ['error'], @@ -28,7 +28,7 @@ module.exports = { 'no-irregular-whitespace': ['error'], 'no-new': ['error'], 'no-undef': ['error'], - 'no-unused-vars': ['error', {'vars': 'all', 'args': 'none'}], + 'no-unused-vars': ['error', {vars: 'all', args: 'none'}], semi: ['error'], strict: ['error'], 'wrap-iife': ['error', 'any'] diff --git a/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js index 91eaf70fbdc..84b1b007a1f 100644 --- a/Tools/eslint-config-cesium/node.js +++ b/Tools/eslint-config-cesium/node.js @@ -1,8 +1,8 @@ 'use strict'; module.exports = { - 'extends': 'cesium', - 'env': { - 'node': true + extends: 'cesium', + env: { + node: true } }; From b9e80426219065b8e4210ea9ecf096d562024b27 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 6 Jun 2017 18:13:28 -0400 Subject: [PATCH 28/41] Cleanup --- CHANGES.md | 6 +- Source/Core/CesiumTerrainProvider.js | 4 +- Source/Core/EllipsoidTerrainProvider.js | 2 +- Source/Core/GoogleEarthEnterpriseMetadata.js | 8 +- .../Core/GoogleEarthEnterpriseTerrainProvider.js | 4 +- Source/Core/Heap.js | 30 ++- Source/Core/Request.js | 71 ++++--- Source/Core/RequestScheduler.js | 211 +++++++++++---------- Source/Core/RequestState.js | 58 +++++- Source/Core/TerrainProvider.js | 2 +- Source/Core/VRTheWorldTerrainProvider.js | 4 +- Source/Core/loadArrayBuffer.js | 2 +- Source/Core/loadBlob.js | 2 +- Source/Core/loadCRN.js | 2 +- Source/Core/loadImage.js | 4 +- Source/Core/loadImageViaBlob.js | 2 +- Source/Core/loadJson.js | 2 +- Source/Core/loadJsonp.js | 2 +- Source/Core/loadKTX.js | 2 +- Source/Core/loadText.js | 2 +- Source/Core/loadWithXhr.js | 9 +- Source/Core/loadXML.js | 2 +- Source/Scene/ArcGisMapServerImageryProvider.js | 2 +- Source/Scene/BingMapsImageryProvider.js | 2 +- Source/Scene/GlobeSurfaceTile.js | 15 +- .../Scene/GoogleEarthEnterpriseImageryProvider.js | 4 +- Source/Scene/GoogleEarthEnterpriseMapsProvider.js | 2 +- Source/Scene/GridImageryProvider.js | 2 +- Source/Scene/Imagery.js | 9 +- Source/Scene/ImageryLayer.js | 6 +- Source/Scene/ImageryProvider.js | 4 +- Source/Scene/MapboxImageryProvider.js | 2 +- Source/Scene/QuadtreePrimitive.js | 1 - Source/Scene/QuadtreeTile.js | 1 + Source/Scene/SingleTileImageryProvider.js | 2 +- Source/Scene/TileCoordinatesImageryProvider.js | 2 +- Source/Scene/TileImagery.js | 4 +- Source/Scene/TileTerrain.js | 13 +- Source/Scene/UrlTemplateImageryProvider.js | 2 +- Source/Scene/WebMapServiceImageryProvider.js | 2 +- Source/Scene/WebMapTileServiceImageryProvider.js | 2 +- Specs/Core/HeapSpec.js | 24 ++- Specs/Core/RequestSchedulerSpec.js | 101 +++++----- 43 files changed, 360 insertions(+), 273 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 36aad1f95da..952a44d2595 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,8 +5,10 @@ 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.36, 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` is deprecated. The same applies for the `url` parameter for `loadArrayBuffer`, `loadBlob`, `loadImageViaBlob`, `loadText`, `loadJson`, `loadXML`, `loadImage`, `loadCRN`, `loadKTX`, and `loadCubeMap`. This will be removed in 1.36, instead `url` must be a string. + * 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`. ### 1.34 - 2017-06-01 diff --git a/Source/Core/CesiumTerrainProvider.js b/Source/Core/CesiumTerrainProvider.js index 677b47897af..11fb68504de 100644 --- a/Source/Core/CesiumTerrainProvider.js +++ b/Source/Core/CesiumTerrainProvider.js @@ -493,7 +493,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 {Request} [request] The request object. + * @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 @@ -534,7 +534,7 @@ define([ } if (typeof request === 'boolean') { - deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.36.'); + 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, diff --git a/Source/Core/EllipsoidTerrainProvider.js b/Source/Core/EllipsoidTerrainProvider.js index 31189d7ec51..58adf683ca8 100644 --- a/Source/Core/EllipsoidTerrainProvider.js +++ b/Source/Core/EllipsoidTerrainProvider.js @@ -152,7 +152,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 {Request} [request] The request object. + * @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 diff --git a/Source/Core/GoogleEarthEnterpriseMetadata.js b/Source/Core/GoogleEarthEnterpriseMetadata.js index be0b51f72dd..d5ee5dd6185 100644 --- a/Source/Core/GoogleEarthEnterpriseMetadata.js +++ b/Source/Core/GoogleEarthEnterpriseMetadata.js @@ -294,7 +294,7 @@ 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 {Request} [request] The request object. + * @param {Request} [request] The request object. Intended for internal use only. * * @private */ @@ -373,7 +373,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. + * @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 * @@ -409,7 +409,7 @@ define([ throttle : request.throttle, throttleByServer : request.throttleByServer, type : request.type, - distance : request.distance + priorityFunction : request.priorityFunction }); return populateSubtree(that, quadKey, subtreeRequest); }); @@ -439,7 +439,7 @@ define([ throttle : request.throttle, throttleByServer : request.throttleByServer, type : request.type, - distance : request.distance + priorityFunction : request.priorityFunction }); return populateSubtree(that, quadKey, subtreeRequest); }) diff --git a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js index 5ed226e6c56..d5e2928633b 100644 --- a/Source/Core/GoogleEarthEnterpriseTerrainProvider.js +++ b/Source/Core/GoogleEarthEnterpriseTerrainProvider.js @@ -346,7 +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 {Request} [request] The request object. + * @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. @@ -448,7 +448,7 @@ define([ sharedRequest = terrainRequests[q]; } else { // Create new request for terrain if (typeof request === 'boolean') { - deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.36.'); + 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, diff --git a/Source/Core/Heap.js b/Source/Core/Heap.js index a455ce6d3cd..77da477e03d 100644 --- a/Source/Core/Heap.js +++ b/Source/Core/Heap.js @@ -18,14 +18,16 @@ define([ * @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 {Function} 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._comparator = options.comparator; this._array = []; this._length = 0; this._maximumLength = undefined; @@ -78,6 +80,19 @@ define([ 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 (Function} + */ + comparator : { + get : function() { + return this._comparator; + } } }); @@ -156,6 +171,7 @@ define([ var array = this._array; var comparator = this._comparator; + var maximumLength = this._maximumLength; var index = this._length++; if (index < array.length) { @@ -176,9 +192,9 @@ define([ var removedElement; - if (defined(this._maximumLength) && (this._length > this._maximumLength)) { - removedElement = array[this.maximumLength]; - this._length = this._maximumLength; + if (defined(maximumLength) && (this._length > maximumLength)) { + removedElement = array[maximumLength]; + this._length = maximumLength; } return removedElement; diff --git a/Source/Core/Request.js b/Source/Core/Request.js index 7661249d056..44a263bc783 100644 --- a/Source/Core/Request.js +++ b/Source/Core/Request.js @@ -14,7 +14,7 @@ define([ 'use strict'; /** - * Stores information for making a request. This should not be constructed directly. Instead, call the appropriate load function, like `loadWithXhr`, `loadImage`, etc. + * Stores information for making a request. In general this does not need to be constructed directly. * * @alias Request * @constructor @@ -22,11 +22,12 @@ define([ * @param {Object} [options] An object with the following properties: * @param {Boolean} [options.url] The url to request. * @param {Function} [options.requestFunction] The actual function that makes the request. The function takes no arguments and returns a promise for the requested data. + * @param {Function} [options.cancelFunction] Function to call when a request is cancelled. The function takes no arguments. + * @param {Function} [options.priorityFunction] Function that is called when the request is updated. The function takes no arguments and returns the updated priority value. + * @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. - * @param {Number} [options.distance=0.0] The distance from the camera, used to prioritize requests. - * @param {Number} [options.screenSpaceError=0.0] The screen space error, used to prioritize requests. */ function Request(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); @@ -45,12 +46,36 @@ define([ * The actual function that makes the request. The function takes no arguments and returns a promise for the requested data. * * @type {Function} - * - * @private */ this.requestFunction = options.requestFunction; /** + * Function to call when a request is cancelled. The function takes no arguments. + * + * @type {Function} + */ + this.cancelFunction = options.cancelFunction; + + /** + * Function that is called when the request is updated. The function takes no arguments and returns the updated priority value. + * + * @type {Function} + */ + this.priorityFunction = options.priorityFunction; + + /** + * 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.priority = defaultValue(options.priority, 0.0); + + /** * 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. * @@ -84,53 +109,23 @@ define([ this.type = defaultValue(options.type, RequestType.OTHER); /** - * The distance from the camera, used to prioritize requests. This value may be edited continually to update - * the request's priority. - * - * @type {Number} - * - * @default 0.0 - */ - this.distance = defaultValue(options.distance, 0.0); - - /** - * The screen space error, used to prioritize requests. This value may be edited continually to update - * the request's priority. - * - * @type {Number} - * - * @default 0.0 - */ - this.screenSpaceError = defaultValue(options.screenSpaceError, 0.0); - - /** - * The request server, derived from the url. + * 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.server = undefined; + this.serverKey = undefined; /** * The current state of the request. * * @type {RequestState} - * - * @private + * @readonly */ this.state = RequestState.UNISSUED; /** - * Reference to the underlying XMLHttpRequest so that it may be aborted in RequestScheduler. - * - * @type {Object} - * - * @private - */ - this.xhr = undefined; - - /** * The requests's deferred promise. * * @type {Object} diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 2c302924d78..264838a5b82 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -24,7 +24,7 @@ define([ 'use strict'; function sortRequests(a, b) { - return a.distance - b.distance; + return a.priority - b.priority; } var statistics = { @@ -37,16 +37,17 @@ define([ }; var priorityHeapLength = 20; - var requestHeap = new Heap(sortRequests); + var requestHeap = new Heap({ + comparator : sortRequests + }); requestHeap.maximumLength = priorityHeapLength; requestHeap.reserve(priorityHeapLength); - var activeRequests = []; + 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. * @@ -67,7 +68,7 @@ define([ /** * The maximum number of simultaneous active requests per server. Un-throttled requests do not observe this limit. * @type {Number} - * @default 50 + * @default 6 */ RequestScheduler.maximumRequestsPerServer = 6; @@ -76,7 +77,7 @@ define([ * @type {Boolean} * @default true */ - RequestScheduler.debugThrottle = true; + RequestScheduler.throttleRequests = true; /** * When true, log statistics to the console every frame @@ -128,69 +129,14 @@ define([ } }); - /** - * Get the server name from a given url. - * - * @param {String} url The url. - * @returns {String} The server name. - */ - RequestScheduler.getServer = function(url) { - //>>includeStart('debug', pragmas.debug); - 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 length = numberOfActiveRequestsByServer[serverName]; - if (!defined(length)) { - numberOfActiveRequestsByServer[serverName] = 0; - } - - return serverName; - }; - - /** - * For testing only. - * - * @private - */ - RequestScheduler.clearForSpecs = function() { - while (requestHeap.length > 0) { - var request = requestHeap.pop(); - cancelRequest(request); + function updatePriority(request) { + if (defined(request.priorityFunction)) { + request.priority = request.priorityFunction(); } - 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; - }; - - /** - * For testing only. - * - * @private - */ - RequestScheduler.numberOfActiveRequestsByServer = function(serverName) { - return numberOfActiveRequestsByServer[serverName]; - }; + } - function serverHasOpenSlots(server) { - return numberOfActiveRequestsByServer[server] < RequestScheduler.maximumRequestsPerServer; + function serverHasOpenSlots(serverKey) { + return numberOfActiveRequestsByServer[serverKey] < RequestScheduler.maximumRequestsPerServer; } function issueRequest(request) { @@ -204,14 +150,11 @@ define([ 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; } - - if (request.state !== RequestState.IGNORED) { - --statistics.numberOfActiveRequests; - --numberOfActiveRequestsByServer[request.server]; - } - + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.serverKey]; request.state = RequestState.RECEIVED; request.deferred.resolve(results); }; @@ -220,15 +163,12 @@ define([ 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; } - - if (request.state !== RequestState.IGNORED) { - ++statistics.numberOfFailedRequests; - --statistics.numberOfActiveRequests; - --numberOfActiveRequestsByServer[request.server]; - } - + ++statistics.numberOfFailedRequests; + --statistics.numberOfActiveRequests; + --numberOfActiveRequestsByServer[request.serverKey]; request.state = RequestState.FAILED; request.deferred.reject(error); }; @@ -240,8 +180,7 @@ define([ activeRequests.push(request); ++statistics.numberOfActiveRequests; ++statistics.numberOfActiveRequestsEver; - ++numberOfActiveRequestsByServer[request.server]; - + ++numberOfActiveRequestsByServer[request.serverKey]; request.requestFunction().then(getRequestReceivedFunction(request)).otherwise(getRequestFailedFunction(request)); return promise; } @@ -250,32 +189,30 @@ define([ var active = request.state === RequestState.ACTIVE; request.state = RequestState.CANCELLED; ++statistics.numberOfCancelledRequests; - request.deferred.reject('Cancelled'); + request.deferred.reject(); if (active) { - // Despite the Request being cancelled, the xhr request is still in flight. - // When it resolves getRequestReceivedFunction or getRequestFailedFunction will ignore it. --statistics.numberOfActiveRequests; - --numberOfActiveRequestsByServer[request.server]; + --numberOfActiveRequestsByServer[request.serverKey]; ++statistics.numberOfCancelledActiveRequests; - if (defined(request.xhr)) { - request.xhr.abort(); - } + } + + if (defined(request.cancelFunction)) { + request.cancelFunction(); } } /** - * Issuers of a request should update properties of requests. At the end of the frame, - * RequestScheduler.update is called to start, cancel, or defer requests. + * 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. - // If an active request is cancelled, its XMLHttpRequest will be aborted. var removeCount = 0; var activeLength = activeRequests.length; - for (var i = 0; i < activeLength; ++i) { + for (i = 0; i < activeLength; ++i) { request = activeRequests[i]; if (request.cancelled) { // Request was explicitly cancelled @@ -293,7 +230,12 @@ define([ } activeRequests.length -= removeCount; - // Resort the heap since priority may have changed. Distance and sse are updated prior to getting here. + // 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. @@ -309,7 +251,7 @@ define([ continue; } - if (request.throttleByServer && !serverHasOpenSlots(request.server)) { + 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; @@ -323,6 +265,33 @@ define([ }; /** + * Get the server key from a given url. + * + * @param {String} url The url. + * @returns {String} The server key. + */ + RequestScheduler.getServerKey = function(url) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('url', url); + //>>includeEnd('debug'); + + var uri = new Uri(url).resolve(pageUri); + uri.normalize(); + 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'); + } + + var length = numberOfActiveRequestsByServer[serverKey]; + if (!defined(length)) { + numberOfActiveRequestsByServer[serverKey] = 0; + } + + return serverKey; + }; + + /** * 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. * @@ -344,11 +313,11 @@ define([ ++statistics.numberOfAttemptedRequests; - if (!defined(request.server)) { - request.server = RequestScheduler.getServer(request.url); + if (!defined(request.serverKey)) { + request.serverKey = RequestScheduler.getServerKey(request.url); } - if (!RequestScheduler.debugThrottle || !request.throttle) { + if (!RequestScheduler.throttleRequests || !request.throttle) { return startRequest(request); } @@ -357,13 +326,14 @@ define([ return undefined; } - if (request.throttleByServer && !serverHasOpenSlots(request.server)) { + if (request.throttleByServer && !serverHasOpenSlots(request.serverKey)) { // Server is saturated. Try again later. return undefined; } // 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 (defined(removedRequest)) { @@ -408,8 +378,47 @@ define([ clearStatistics(); } - // For testing - RequestScheduler._requestHeap = requestHeap; + /** + * For testing only. Clears any requests that may not have completed from previous tests. + * + * @private + */ + RequestScheduler.clearForSpecs = function() { + 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; + }; + + /** + * For testing only. + * + * @private + */ + RequestScheduler.numberOfActiveRequestsByServer = function(serverKey) { + return numberOfActiveRequestsByServer[serverKey]; + }; + + /** + * For testing only. + * + * @private + */ + RequestScheduler.requestHeap = requestHeap; return RequestScheduler; }); diff --git a/Source/Core/RequestState.js b/Source/Core/RequestState.js index 7cd28c4e65d..23498ef57fc 100644 --- a/Source/Core/RequestState.js +++ b/Source/Core/RequestState.js @@ -6,16 +6,58 @@ define([ 'use strict'; /** - * @private + * State of the request. + * + * @exports RequestState */ var RequestState = { - UNISSUED : 0, // Initial unissued state. - ISSUED : 1, // Issued but not yet active. Will become active when open slots are available. - ACTIVE : 2, // Actual http request has been sent. - RECEIVED : 3, // Request completed successfully. - CANCELLED : 4, // Request was cancelled, either explicitly or automatically because of low priority. - FAILED : 5, // Request failed. - IGNORED : 6 // For RequestScheduler.clearForSpecs - lets requests finish but doesn't contribute to statistics. + /** + * 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/TerrainProvider.js b/Source/Core/TerrainProvider.js index bfba9cf039d..6bdc2e00001 100644 --- a/Source/Core/TerrainProvider.js +++ b/Source/Core/TerrainProvider.js @@ -198,7 +198,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 {Request} [request] The request object. + * @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 diff --git a/Source/Core/VRTheWorldTerrainProvider.js b/Source/Core/VRTheWorldTerrainProvider.js index 79523b4afe6..5339deb9c6c 100644 --- a/Source/Core/VRTheWorldTerrainProvider.js +++ b/Source/Core/VRTheWorldTerrainProvider.js @@ -260,7 +260,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 {Request} [request] The request object. + * @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. @@ -281,7 +281,7 @@ define([ } if (typeof request === 'boolean') { - deprecationWarning('throttleRequests', 'The throttleRequest parameter for requestTileGeometry was deprecated in Cesium 1.35. It will be removed in 1.36.'); + 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, diff --git a/Source/Core/loadArrayBuffer.js b/Source/Core/loadArrayBuffer.js index d86038eff30..cc56d573ff6 100644 --- a/Source/Core/loadArrayBuffer.js +++ b/Source/Core/loadArrayBuffer.js @@ -15,7 +15,7 @@ define([ * * @param {String} url The URL of the binary data. * @param {Object} [headers] HTTP headers to send with the requests. - * @param {Request} [request] The request object. + * @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 diff --git a/Source/Core/loadBlob.js b/Source/Core/loadBlob.js index aad892afdbd..0e136d22c37 100644 --- a/Source/Core/loadBlob.js +++ b/Source/Core/loadBlob.js @@ -15,7 +15,7 @@ define([ * * @param {String} url The URL of the data. * @param {Object} [headers] HTTP headers to send with the requests. - * @param {Request} [request] The request object. + * @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 diff --git a/Source/Core/loadCRN.js b/Source/Core/loadCRN.js index f01270bb6ff..eac7121e100 100644 --- a/Source/Core/loadCRN.js +++ b/Source/Core/loadCRN.js @@ -28,7 +28,7 @@ define([ * * @param {String|ArrayBuffer} urlOrBuffer The URL of the binary data or an ArrayBuffer. * @param {Object} [headers] HTTP headers to send with the requests. - * @param {Request} [request] The request object. + * @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. diff --git a/Source/Core/loadImage.js b/Source/Core/loadImage.js index b726ed80056..79c0c95673a 100644 --- a/Source/Core/loadImage.js +++ b/Source/Core/loadImage.js @@ -35,7 +35,7 @@ define([ * @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. - * @param {Request} [request] The request object. + * @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. * * @@ -65,7 +65,7 @@ define([ 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 Cesium 1.36'); + 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); }); diff --git a/Source/Core/loadImageViaBlob.js b/Source/Core/loadImageViaBlob.js index 7f2e12ecc23..9d2d3292453 100644 --- a/Source/Core/loadImageViaBlob.js +++ b/Source/Core/loadImageViaBlob.js @@ -28,7 +28,7 @@ define([ * @exports loadImageViaBlob * * @param {String} url The source URL of the image. - * @param {Request} [request] The request object. + * @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. * * diff --git a/Source/Core/loadJson.js b/Source/Core/loadJson.js index f3215e59448..1fd1de5133c 100644 --- a/Source/Core/loadJson.js +++ b/Source/Core/loadJson.js @@ -30,7 +30,7 @@ define([ * @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. - * @param {Request} [request] The request object. + * @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. * * diff --git a/Source/Core/loadJsonp.js b/Source/Core/loadJsonp.js index 5e721769340..c63f3b58b0b 100644 --- a/Source/Core/loadJsonp.js +++ b/Source/Core/loadJsonp.js @@ -33,7 +33,7 @@ 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. - * @param {Request} [request] The request object. + * @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. * * diff --git a/Source/Core/loadKTX.js b/Source/Core/loadKTX.js index 9be69bb2bb8..cd8600b10d1 100644 --- a/Source/Core/loadKTX.js +++ b/Source/Core/loadKTX.js @@ -39,7 +39,7 @@ define([ * * @param {String|ArrayBuffer} urlOrBuffer The URL of the binary data or an ArrayBuffer. * @param {Object} [headers] HTTP headers to send with the requests. - * @param {Request} [request] The request object. + * @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. diff --git a/Source/Core/loadText.js b/Source/Core/loadText.js index a92607405ed..8ca386fe1bf 100644 --- a/Source/Core/loadText.js +++ b/Source/Core/loadText.js @@ -15,7 +15,7 @@ define([ * * @param {String} url The URL to request. * @param {Object} [headers] HTTP headers to send with the request. - * @param {Request} [request] The request object. + * @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. * * diff --git a/Source/Core/loadWithXhr.js b/Source/Core/loadWithXhr.js index d2102b50bac..789b737607a 100644 --- a/Source/Core/loadWithXhr.js +++ b/Source/Core/loadWithXhr.js @@ -74,7 +74,7 @@ define([ 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.36'); + 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); }); @@ -95,7 +95,12 @@ define([ request.url = url; request.requestFunction = function() { var deferred = when.defer(); - request.xhr = loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); + var xhr = loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); + if (defined(xhr)) { + request.cancelFunction = function() { + xhr.abort(); + }; + } return deferred.promise; }; diff --git a/Source/Core/loadXML.js b/Source/Core/loadXML.js index e43ef006436..780ff6ad5ee 100644 --- a/Source/Core/loadXML.js +++ b/Source/Core/loadXML.js @@ -15,7 +15,7 @@ define([ * * @param {String} url The URL to request. * @param {Object} [headers] HTTP headers to send with the request. - * @param {Request} [request] The request object. + * @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. * * diff --git a/Source/Scene/ArcGisMapServerImageryProvider.js b/Source/Scene/ArcGisMapServerImageryProvider.js index 0970ce28b2e..fa53f26b766 100644 --- a/Source/Scene/ArcGisMapServerImageryProvider.js +++ b/Source/Scene/ArcGisMapServerImageryProvider.js @@ -579,7 +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. + * @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 diff --git a/Source/Scene/BingMapsImageryProvider.js b/Source/Scene/BingMapsImageryProvider.js index 3dea11dc2da..fdcf1531b7c 100644 --- a/Source/Scene/BingMapsImageryProvider.js +++ b/Source/Scene/BingMapsImageryProvider.js @@ -522,7 +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. + * @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 diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 90b0da7a002..1e27a4df4f8 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -252,6 +252,12 @@ define([ }); } + 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)) { @@ -261,8 +267,10 @@ define([ surfaceTile.tileBoundingRegion = createTileBoundingRegion(tile); } - // Update distance while the tile loads - tile._distance = surfaceTile.tileBoundingRegion.distanceToCamera(frameState); + 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) { prepareNewTile(tile, terrainProvider, imageryLayerCollection); @@ -328,6 +336,7 @@ define([ if (isDoneLoading) { tile.state = QuadtreeTileLoadState.DONE; + tile._priorityFunction = undefined; } } }; @@ -360,7 +369,7 @@ define([ var suspendUpsampling = false; if (defined(loaded)) { - loaded.processLoadStateMachine(frameState, terrainProvider, tile.x, tile.y, tile.level, tile._distance); + 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 79c0f4d56df..3dfb87bbcf2 100644 --- a/Source/Scene/GoogleEarthEnterpriseImageryProvider.js +++ b/Source/Scene/GoogleEarthEnterpriseImageryProvider.js @@ -432,7 +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. + * @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 @@ -457,7 +457,7 @@ define([ throttle : request.throttle, throttleByServer : request.throttleByServer, type : request.type, - distance : request.distance + priorityFunction : request.priorityFunction }); metadata.populateSubtree(x, y, level, metadataRequest); return undefined; // No metadata so return undefined so we can be loaded later diff --git a/Source/Scene/GoogleEarthEnterpriseMapsProvider.js b/Source/Scene/GoogleEarthEnterpriseMapsProvider.js index ab4adf47350..4251d3d2d40 100644 --- a/Source/Scene/GoogleEarthEnterpriseMapsProvider.js +++ b/Source/Scene/GoogleEarthEnterpriseMapsProvider.js @@ -538,7 +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. + * @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 diff --git a/Source/Scene/GridImageryProvider.js b/Source/Scene/GridImageryProvider.js index b98e628a84a..7bfa4f16485 100644 --- a/Source/Scene/GridImageryProvider.js +++ b/Source/Scene/GridImageryProvider.js @@ -321,7 +321,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. + * @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 diff --git a/Source/Scene/Imagery.js b/Source/Scene/Imagery.js index d832d9f210a..07414e2ac3e 100644 --- a/Source/Scene/Imagery.js +++ b/Source/Scene/Imagery.js @@ -87,15 +87,10 @@ define([ return this.referenceCount; }; - Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, distance) { + Imagery.prototype.processStateMachine = function(frameState, needGeographicProjection, priorityFunction) { if (this.state === ImageryState.UNLOADED) { this.state = ImageryState.TRANSITIONING; - this.imageryLayer._requestImagery(this, distance); - } - - if (defined(this.request) && this.request.state === RequestState.ISSUED) { - // Update distance while loading to prioritize request - this.request.distance = distance; + this.imageryLayer._requestImagery(this, priorityFunction); } if (this.state === ImageryState.RECEIVED) { diff --git a/Source/Scene/ImageryLayer.js b/Source/Scene/ImageryLayer.js index 038826aaea0..39d7014322f 100644 --- a/Source/Scene/ImageryLayer.js +++ b/Source/Scene/ImageryLayer.js @@ -662,9 +662,9 @@ define([ * @private * * @param {Imagery} imagery The imagery to request. - * @param {Number} [distance] The distance of the tile from the camera. + * @param {Function} [priorityFunction] The priority function used for sorting the imagery request. */ - ImageryLayer.prototype._requestImagery = function(imagery, distance) { + ImageryLayer.prototype._requestImagery = function(imagery, priorityFunction) { var imageryProvider = this._imageryProvider; var that = this; @@ -710,7 +710,7 @@ define([ throttle : true, throttleByServer : true, type : RequestType.IMAGERY, - distance : distance + priorityFunction : priorityFunction }); imagery.request = request; imagery.state = ImageryState.TRANSITIONING; diff --git a/Source/Scene/ImageryProvider.js b/Source/Scene/ImageryProvider.js index b73b5adfddb..36c5a2eb573 100644 --- a/Source/Scene/ImageryProvider.js +++ b/Source/Scene/ImageryProvider.js @@ -266,7 +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. + * @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 @@ -307,7 +307,7 @@ define([ * * @param {ImageryProvider} imageryProvider The imagery provider for the URL. * @param {String} url The URL of the image. - * @param {Request} [request] The request object. + * @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 diff --git a/Source/Scene/MapboxImageryProvider.js b/Source/Scene/MapboxImageryProvider.js index 5c32d2082b6..ac2949df0c5 100644 --- a/Source/Scene/MapboxImageryProvider.js +++ b/Source/Scene/MapboxImageryProvider.js @@ -317,7 +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. + * @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 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/SingleTileImageryProvider.js b/Source/Scene/SingleTileImageryProvider.js index ca9486e0f1f..9e869c6b5b6 100644 --- a/Source/Scene/SingleTileImageryProvider.js +++ b/Source/Scene/SingleTileImageryProvider.js @@ -370,7 +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. + * @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 diff --git a/Source/Scene/TileCoordinatesImageryProvider.js b/Source/Scene/TileCoordinatesImageryProvider.js index 52e4ebc2b2b..0ecf383b481 100644 --- a/Source/Scene/TileCoordinatesImageryProvider.js +++ b/Source/Scene/TileCoordinatesImageryProvider.js @@ -240,7 +240,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. + * @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 diff --git a/Source/Scene/TileImagery.js b/Source/Scene/TileImagery.js index 282b44cda6a..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, tile._distance); + 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, tile._distance); + 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 8e1a4b12c0f..98c315518c9 100644 --- a/Source/Scene/TileTerrain.js +++ b/Source/Scene/TileTerrain.js @@ -93,14 +93,9 @@ define([ tile.data.occludeePointInScaledSpace = Cartesian3.clone(mesh.occludeePointInScaledSpace, surfaceTile.occludeePointInScaledSpace); }; - TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level, distance) { + TileTerrain.prototype.processLoadStateMachine = function(frameState, terrainProvider, x, y, level, priorityFunction) { if (this.state === TerrainState.UNLOADED) { - requestTileGeometry(this, terrainProvider, x, y, level, distance); - } - - if (defined(this.request) && this.request.state === RequestState.ISSUED) { - // Update distance while loading to prioritize request - this.request.distance = distance; + requestTileGeometry(this, terrainProvider, x, y, level, priorityFunction); } if (this.state === TerrainState.RECEIVED) { @@ -112,7 +107,7 @@ define([ } }; - function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, distance) { + function requestTileGeometry(tileTerrain, terrainProvider, x, y, level, priorityFunction) { function success(terrainData) { tileTerrain.data = terrainData; tileTerrain.state = TerrainState.RECEIVED; @@ -149,7 +144,7 @@ define([ throttle : true, throttleByServer : true, type : RequestType.TERRAIN, - distance : distance + priorityFunction : priorityFunction }); tileTerrain.request = request; tileTerrain.data = terrainProvider.requestTileGeometry(x, y, level, request); diff --git a/Source/Scene/UrlTemplateImageryProvider.js b/Source/Scene/UrlTemplateImageryProvider.js index 5ab353eab4e..6d1edf87cca 100644 --- a/Source/Scene/UrlTemplateImageryProvider.js +++ b/Source/Scene/UrlTemplateImageryProvider.js @@ -601,7 +601,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. + * @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 diff --git a/Source/Scene/WebMapServiceImageryProvider.js b/Source/Scene/WebMapServiceImageryProvider.js index 93f3c83dc43..93e98bc4a01 100644 --- a/Source/Scene/WebMapServiceImageryProvider.js +++ b/Source/Scene/WebMapServiceImageryProvider.js @@ -433,7 +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. + * @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 diff --git a/Source/Scene/WebMapTileServiceImageryProvider.js b/Source/Scene/WebMapTileServiceImageryProvider.js index 2da7cd3fb36..21986a61866 100644 --- a/Source/Scene/WebMapTileServiceImageryProvider.js +++ b/Source/Scene/WebMapTileServiceImageryProvider.js @@ -432,7 +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. + * @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 diff --git a/Specs/Core/HeapSpec.js b/Specs/Core/HeapSpec.js index a3af65f7184..4d41e7e48fb 100644 --- a/Specs/Core/HeapSpec.js +++ b/Specs/Core/HeapSpec.js @@ -30,7 +30,9 @@ 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()); @@ -41,7 +43,9 @@ defineSuite([ }); 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()); @@ -55,7 +59,9 @@ defineSuite([ }); it('limited by maximum length', function() { - var heap = new Heap(comparator); + var heap = new Heap({ + comparator : comparator + }); heap.maximumLength = length / 2; var pass = true; for (var i = 0; i < length; ++i) { @@ -69,7 +75,9 @@ defineSuite([ }); 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()); @@ -85,7 +93,9 @@ defineSuite([ }); it('insert returns the removed element when maximumLength is set', function() { - var heap = new Heap(comparator); + var heap = new Heap({ + comparator : comparator + }); heap.maximumLength = length; var i; @@ -123,7 +133,9 @@ defineSuite([ } var i; - var heap = new Heap(comparator); + var heap = new Heap({ + comparator : comparator + }); for (i = 0; i < length; ++i) { heap.insert({ distance : i / (length - 1), diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index 63a7e1a90aa..4dfff78ecff 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -59,17 +59,17 @@ defineSuite([ it('getServer throws if url is undefined', function() { expect(function() { - RequestScheduler.getServer(); + RequestScheduler.getServerKey(); }).toThrowDeveloperError(); }); it('getServer with https', function() { - var server = RequestScheduler.getServer('https://foo.com/1'); + var server = RequestScheduler.getServerKey('https://foo.com/1'); expect(server).toEqual('foo.com:443'); }); it('getServer with http', function() { - var server = RequestScheduler.getServer('http://foo.com/1'); + var server = RequestScheduler.getServerKey('http://foo.com/1'); expect(server).toEqual('foo.com:80'); }); @@ -151,7 +151,7 @@ defineSuite([ } var url = 'http://foo.com/1'; - var server = RequestScheduler.getServer(url); + var server = RequestScheduler.getServerKey(url); function createRequest() { return new Request({ @@ -217,12 +217,12 @@ defineSuite([ return deferred.promise; } - function createRequest(distance) { + function createRequest(priority) { var request = new Request({ url : 'http://foo.com/1', requestFunction : requestFunction, throttle : true, - distance : distance + priority : priority }); requests.push(request); return request; @@ -271,11 +271,11 @@ defineSuite([ expect(promise).toBeDefined(); if (dataOrBlobUri) { - expect(request.server).toBeUndefined(); + expect(request.serverKey).toBeUndefined(); expect(statistics.numberOfActiveRequests).toBe(0); } else { expect(statistics.numberOfActiveRequests).toBe(1); - expect(RequestScheduler.numberOfActiveRequestsByServer(request.server)).toBe(1); + expect(RequestScheduler.numberOfActiveRequestsByServer(request.serverKey)).toBe(1); } deferreds[0].resolve(); @@ -284,7 +284,7 @@ defineSuite([ expect(request.state).toBe(RequestState.RECEIVED); expect(statistics.numberOfActiveRequests).toBe(0); if (!dataOrBlobUri) { - expect(RequestScheduler.numberOfActiveRequestsByServer(request.server)).toBe(0); + expect(RequestScheduler.numberOfActiveRequestsByServer(request.serverKey)).toBe(0); } }); } @@ -362,28 +362,23 @@ defineSuite([ return promise.then(function() { fail('should not be called'); }).otherwise(function(error) { - expect(error).toBe('Cancelled'); + expect(request.state).toBe(RequestState.CANCELLED); }); }); it('cancels an active request', function() { var statistics = RequestScheduler.statistics; - var aborted = true; - var mockXhr = { - abort : function() { - aborted = true; - } - }; + var cancelFunction = jasmine.createSpy('cancelFunction'); function requestFunction() { - request.xhr = mockXhr; return when.defer().promise; } var request = new Request({ throttle : true, url : 'https://foo.com/1', - requestFunction : requestFunction + requestFunction : requestFunction, + cancelFunction : cancelFunction }); var promise = RequestScheduler.request(request); @@ -396,13 +391,13 @@ defineSuite([ expect(request.state).toBe(RequestState.CANCELLED); expect(statistics.numberOfCancelledRequests).toBe(1); expect(statistics.numberOfCancelledActiveRequests).toBe(1); - expect(RequestScheduler.numberOfActiveRequestsByServer(request.server)).toBe(0); - expect(aborted).toBe(true); + expect(RequestScheduler.numberOfActiveRequestsByServer(request.serverKey)).toBe(0); + expect(cancelFunction).toHaveBeenCalled(); return promise.then(function() { fail('should not be called'); }).otherwise(function(error) { - expect(error).toBe('Cancelled'); + expect(request.state).toBe(RequestState.CANCELLED); }); }); @@ -436,47 +431,58 @@ defineSuite([ }); }); - it('prioritizes requests by distance', function() { - var currentDistance = 0.0; + it('prioritizes requests', function() { + var currentPriority = 0.0; - function getRequestFunction(distance) { + function getRequestFunction(priority) { return function() { - expect(distance).toBeGreaterThan(currentDistance); - currentDistance = distance; + expect(priority).toBeGreaterThan(currentPriority); + currentPriority = priority; return when.resolve(); }; } - function createRequest(distance) { + function createRequest(priority) { return new Request({ throttle : true, url : 'https://foo.com/1', - requestFunction : getRequestFunction(distance), - distance : distance + requestFunction : getRequestFunction(priority), + priority : priority }); } var length = RequestScheduler.priorityHeapLength; for (var i = 0; i < length; ++i) { - var distance = Math.random(); - RequestScheduler.request(createRequest(distance)); + var priority = Math.random(); + RequestScheduler.request(createRequest(priority)); } RequestScheduler.update(); - expect(currentDistance).toBeGreaterThan(0.0); // Ensures that the expect in getRequestFunction is actually called + expect(currentPriority).toBeGreaterThan(0.0); // Ensures that the expect in getRequestFunction is actually called }); it('updates priority', function() { + var invertPriority = false; + + function getPriorityFunction(priority) { + return function() { + if (invertPriority) { + return 1.0 - priority; + } + return priority; + }; + } + function requestFunction() { return when.resolve(); } - function createRequest(distance) { + function createRequest(priority) { return new Request({ throttle : true, url : 'https://foo.com/1', requestFunction : requestFunction, - distance : distance + priorityFunction : getPriorityFunction(priority) }); } @@ -484,16 +490,17 @@ defineSuite([ var request; var length = RequestScheduler.priorityHeapLength; for (i = 0; i < length; ++i) { - var distance = i / (length - 1); - request = createRequest(distance); + var priority = i / (length - 1); + request = createRequest(priority); request.testId = i; RequestScheduler.request(request); } + // Update priorities while not letting any requests go through RequestScheduler.maximumRequests = 0; RequestScheduler.update(); - var requestHeap = RequestScheduler._requestHeap; + var requestHeap = RequestScheduler.requestHeap; var requests = []; var currentTestId = 0; while (requestHeap.length > 0) { @@ -507,11 +514,9 @@ defineSuite([ requestHeap.insert(requests[i]); } - for (i = 0; i < length; ++i) { - requests[i].distance = 1.0 - requests[i].distance; // Invert priority - } - + invertPriority = true; RequestScheduler.update(); + while (requestHeap.length > 0) { request = requestHeap.pop(); expect(request.testId).toBeLessThanOrEqualTo(currentTestId); @@ -524,12 +529,12 @@ defineSuite([ return when.resolve(); } - function createRequest(distance) { + function createRequest(priority) { return new Request({ throttle : true, url : 'https://foo.com/1', requestFunction : requestFunction, - distance : distance + priority : priority }); } @@ -625,14 +630,14 @@ defineSuite([ } }); - it('debugThrottle', function() { + it('throttleRequests', function() { RequestScheduler.maximumRequests = 0; function requestFunction() { return when.resolve(); } - RequestScheduler.debugThrottle = true; + RequestScheduler.throttleRequests = true; var request = new Request({ throttle : true, url : 'https://foo.com/1', @@ -641,7 +646,7 @@ defineSuite([ var promise = RequestScheduler.request(request); expect(promise).toBeUndefined(); - RequestScheduler.debugThrottle = false; + RequestScheduler.throttleRequests = false; request = new Request({ throttle : true, url : 'https://foo.com/1', @@ -650,7 +655,7 @@ defineSuite([ promise = RequestScheduler.request(request); expect(promise).toBeDefined(); - RequestScheduler.debugThrottle = true; + RequestScheduler.throttleRequests = true; }); it('debugShowStatistics', function() { @@ -693,5 +698,7 @@ defineSuite([ for (var i = 0; i < length; ++i) { deferreds[i].resolve(); } + + RequestScheduler.debugShowStatistics = false; }); }); From ac2ba5ca37144e347c210af3e52ac49d2a9e49b1 Mon Sep 17 00:00:00 2001 From: Matthew Amato Date: Wed, 7 Jun 2017 19:44:14 -0400 Subject: [PATCH 29/41] Make sure camera world coordinate properties are normalized The camera would not properly normalize the WC versions of up, right, and direction, which could lead to potential problems later in the frame. --- Source/Scene/Camera.js | 3 +++ Specs/Scene/CameraSpec.js | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index 5e3180a55f5..d41ce2619e2 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) { diff --git a/Specs/Scene/CameraSpec.js b/Specs/Scene/CameraSpec.js index 21403eb8147..ca983e47c81 100644 --- a/Specs/Scene/CameraSpec.js +++ b/Specs/Scene/CameraSpec.js @@ -2850,4 +2850,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); + }); }); From 24f609ba386b89500c1a7b96c34e06db4dce323a Mon Sep 17 00:00:00 2001 From: Matthew Amato Date: Wed, 7 Jun 2017 20:07:54 -0400 Subject: [PATCH 30/41] Use new DomOrientationEvent constructor when available. Chrome 59 removed `event.initDeviceOrientationEvent` in favor of a `DeviceOrientationEvent` constructor, but the former is required for other browsers. This is a test-only change. Fixes #5430 --- Specs/DomEventSimulator.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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; } From 0b3b1ccc3d233c4956829e9a0e31b49cb35bf07d Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 7 Jun 2017 20:19:18 -0400 Subject: [PATCH 31/41] Improved doc --- Source/Core/Heap.js | 12 ++++++++++-- Source/Core/Request.js | 35 ++++++++++++++++++++++++++--------- Source/Core/RequestType.js | 27 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/Source/Core/Heap.js b/Source/Core/Heap.js index 77da477e03d..fa44fc58b88 100644 --- a/Source/Core/Heap.js +++ b/Source/Core/Heap.js @@ -19,7 +19,7 @@ define([ * @private * * @param {Object} options Object with the following properties: - * @param {Function} 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. + * @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(options) { //>>includeStart('debug', pragmas.debug); @@ -87,7 +87,7 @@ define([ * * @memberof Heap.prototype * - * @type (Function} + * @type {Heap~ComparatorCallback} */ comparator : { get : function() { @@ -222,5 +222,13 @@ define([ 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 44a263bc783..42ed344b1c5 100644 --- a/Source/Core/Request.js +++ b/Source/Core/Request.js @@ -21,9 +21,9 @@ define([ * * @param {Object} [options] An object with the following properties: * @param {Boolean} [options.url] The url to request. - * @param {Function} [options.requestFunction] The actual function that makes the request. The function takes no arguments and returns a promise for the requested data. - * @param {Function} [options.cancelFunction] Function to call when a request is cancelled. The function takes no arguments. - * @param {Function} [options.priorityFunction] Function that is called when the request is updated. The function takes no arguments and returns the updated priority value. + * @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. @@ -43,23 +43,23 @@ define([ this.url = options.url; /** - * The actual function that makes the request. The function takes no arguments and returns a promise for the requested data. + * The function that makes the actual data request. * - * @type {Function} + * @type {Request~RequestCallback} */ this.requestFunction = options.requestFunction; /** - * Function to call when a request is cancelled. The function takes no arguments. + * The function that is called when the request is cancelled. * - * @type {Function} + * @type {Request~CancelCallback} */ this.cancelFunction = options.cancelFunction; /** - * Function that is called when the request is updated. The function takes no arguments and returns the updated priority value. + * The function that is called to update the request's priority, which occurs once per frame. * - * @type {Function} + * @type {Request~PriorityCallback} */ this.priorityFunction = options.priorityFunction; @@ -153,5 +153,22 @@ define([ 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/RequestType.js b/Source/Core/RequestType.js index 9beebd2b238..d01348b54d5 100644 --- a/Source/Core/RequestType.js +++ b/Source/Core/RequestType.js @@ -11,9 +11,36 @@ define([ * @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 }; From cfe237f00446d8e5c6af23b75acc4bad9c62cd50 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 7 Jun 2017 20:27:54 -0400 Subject: [PATCH 32/41] Clear request scheduler before each ImageryProvider test --- Specs/Scene/ArcGisMapServerImageryProviderSpec.js | 4 ++++ Specs/Scene/BingMapsImageryProviderSpec.js | 4 ++++ Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js | 4 ++++ Specs/Scene/MapboxImageryProviderSpec.js | 4 ++++ Specs/Scene/UrlTemplateImageryProviderSpec.js | 4 ++++ Specs/Scene/WebMapServiceImageryProviderSpec.js | 4 ++++ Specs/Scene/WebMapTileServiceImageryProviderSpec.js | 4 ++++ Specs/Scene/createOpenStreetMapImageryProviderSpec.js | 4 ++++ Specs/Scene/createTileMapServiceImageryProviderSpec.js | 4 ++++ 9 files changed, 36 insertions(+) diff --git a/Specs/Scene/ArcGisMapServerImageryProviderSpec.js b/Specs/Scene/ArcGisMapServerImageryProviderSpec.js index 1d5d0165307..1dcde473ebb 100644 --- a/Specs/Scene/ArcGisMapServerImageryProviderSpec.js +++ b/Specs/Scene/ArcGisMapServerImageryProviderSpec.js @@ -49,6 +49,10 @@ defineSuite([ Uri) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadJsonp.loadAndExecuteScript = loadJsonp.defaultLoadAndExecuteScript; loadImage.createImage = loadImage.defaultCreateImage; diff --git a/Specs/Scene/BingMapsImageryProviderSpec.js b/Specs/Scene/BingMapsImageryProviderSpec.js index f148419fa40..192a04c2374 100644 --- a/Specs/Scene/BingMapsImageryProviderSpec.js +++ b/Specs/Scene/BingMapsImageryProviderSpec.js @@ -33,6 +33,10 @@ defineSuite([ pollToPromise) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadJsonp.loadAndExecuteScript = loadJsonp.defaultLoadAndExecuteScript; loadImage.createImage = loadImage.defaultCreateImage; diff --git a/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js b/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js index 05cccff9bf6..00eec62d9ac 100644 --- a/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js +++ b/Specs/Scene/GoogleEarthEnterpriseImageryProviderSpec.js @@ -41,6 +41,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + beforeAll(function() { decodeGoogleEarthEnterpriseData.passThroughDataForTesting = true; }); diff --git a/Specs/Scene/MapboxImageryProviderSpec.js b/Specs/Scene/MapboxImageryProviderSpec.js index cab813d0d41..50b681bd441 100644 --- a/Specs/Scene/MapboxImageryProviderSpec.js +++ b/Specs/Scene/MapboxImageryProviderSpec.js @@ -27,6 +27,10 @@ defineSuite([ pollToPromise) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); diff --git a/Specs/Scene/UrlTemplateImageryProviderSpec.js b/Specs/Scene/UrlTemplateImageryProviderSpec.js index dedb6d22d66..f3fbe8d77e9 100644 --- a/Specs/Scene/UrlTemplateImageryProviderSpec.js +++ b/Specs/Scene/UrlTemplateImageryProviderSpec.js @@ -37,6 +37,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); diff --git a/Specs/Scene/WebMapServiceImageryProviderSpec.js b/Specs/Scene/WebMapServiceImageryProviderSpec.js index 19cf18665c2..e838d04971d 100644 --- a/Specs/Scene/WebMapServiceImageryProviderSpec.js +++ b/Specs/Scene/WebMapServiceImageryProviderSpec.js @@ -43,6 +43,10 @@ defineSuite([ Uri) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; loadWithXhr.load = loadWithXhr.defaultLoad; diff --git a/Specs/Scene/WebMapTileServiceImageryProviderSpec.js b/Specs/Scene/WebMapTileServiceImageryProviderSpec.js index 239b524c8fa..11398eba7f4 100644 --- a/Specs/Scene/WebMapTileServiceImageryProviderSpec.js +++ b/Specs/Scene/WebMapTileServiceImageryProviderSpec.js @@ -31,6 +31,10 @@ defineSuite([ Uri) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); diff --git a/Specs/Scene/createOpenStreetMapImageryProviderSpec.js b/Specs/Scene/createOpenStreetMapImageryProviderSpec.js index 656b1e378fa..041b175ca4c 100644 --- a/Specs/Scene/createOpenStreetMapImageryProviderSpec.js +++ b/Specs/Scene/createOpenStreetMapImageryProviderSpec.js @@ -27,6 +27,10 @@ defineSuite([ pollToPromise) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; }); diff --git a/Specs/Scene/createTileMapServiceImageryProviderSpec.js b/Specs/Scene/createTileMapServiceImageryProviderSpec.js index 9fa7e58669e..fb1cfc52369 100644 --- a/Specs/Scene/createTileMapServiceImageryProviderSpec.js +++ b/Specs/Scene/createTileMapServiceImageryProviderSpec.js @@ -43,6 +43,10 @@ defineSuite([ when) { 'use strict'; + beforeEach(function() { + RequestScheduler.clearForSpecs(); + }); + afterEach(function() { loadImage.createImage = loadImage.defaultCreateImage; loadWithXhr.load = loadWithXhr.defaultLoad; From 228bb068a30b3277e0572680dbbfbd15f878c30f Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 7 Jun 2017 21:06:15 -0400 Subject: [PATCH 33/41] Fix for tests --- Source/Core/loadWithXhr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/loadWithXhr.js b/Source/Core/loadWithXhr.js index 789b737607a..f5e2690aa77 100644 --- a/Source/Core/loadWithXhr.js +++ b/Source/Core/loadWithXhr.js @@ -96,7 +96,7 @@ define([ request.requestFunction = function() { var deferred = when.defer(); var xhr = loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); - if (defined(xhr)) { + if (defined(xhr) && defined(xhr.abort)) { request.cancelFunction = function() { xhr.abort(); }; From 40b92930d5f615dc8ed7f2c63df7fc1686e98ab8 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 8 Jun 2017 14:27:15 -0400 Subject: [PATCH 34/41] Remove while(true) which fixes eslint error --- Source/Core/Heap.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Core/Heap.js b/Source/Core/Heap.js index fa44fc58b88..11f379213a3 100644 --- a/Source/Core/Heap.js +++ b/Source/Core/Heap.js @@ -123,8 +123,9 @@ define([ var comparator = this._comparator; var array = this._array; var candidate = -1; + var inserting = true; - while (true) { + while (inserting) { var right = 2 * (index + 1); var left = right - 1; @@ -141,7 +142,7 @@ define([ swap(array, candidate, index); index = candidate; } else { - break; + inserting = false; } } }; From 296f7605f3d6723d915789a2a98662fb6ddad271 Mon Sep 17 00:00:00 2001 From: Dan Bagnell Date: Thu, 8 Jun 2017 17:33:00 -0400 Subject: [PATCH 35/41] Update CHANGES.md. --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e11979c66ea..5b4061fb5f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,7 +8,7 @@ Change Log * 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) -* Fixed a bug where `Camera.zoom2D` worked incorrectly when the display height was greater than the display width [#5421] (https://github.com/AnalyticalGraphicsInc/cesium/pull/5421) +* 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) ### 1.34 - 2017-06-01 From b25fbc2e4aecd38f924fc161d65cfd5d86cfda51 Mon Sep 17 00:00:00 2001 From: Srinivas Kaza Date: Fri, 9 Jun 2017 10:06:05 -0400 Subject: [PATCH 36/41] Cleans up remaining commented code and other fixes --- Source/Core/Ellipsoid.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Source/Core/Ellipsoid.js b/Source/Core/Ellipsoid.js index 36001049ba6..d31c69aefaf 100644 --- a/Source/Core/Ellipsoid.js +++ b/Source/Core/Ellipsoid.js @@ -285,14 +285,6 @@ 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'); - }*/ - - //>>includeStart('debug', pragmas.debug); Check.typeOf.object('value', value); Check.defined('array', array); //>>includeEnd('debug'); @@ -534,7 +526,7 @@ define([ */ Ellipsoid.prototype.scaleToGeocentricSurface = function(cartesian, result) { //>>includeStart('debug', pragmas.debug); - Check.defined('cartesian', cartesian); + Check.typeOf.object('cartesian', cartesian); //>>includeEnd('debug'); if (!defined(result)) { @@ -629,7 +621,7 @@ define([ */ Ellipsoid.prototype.getSurfaceNormalIntersectionWithZAxis = function(position, buffer, result) { //>>includeStart('debug', pragmas.debug); - Check.defined('position', position); + 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)'); From 18f4774f96af7d6e8e03632535bb0364c34192b6 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Fri, 9 Jun 2017 10:10:23 -0400 Subject: [PATCH 37/41] Add amd to browser.js, Change unused-vars rules, Add homepage to package.json --- .eslintrc.json | 5 ++++- Tools/eslint-config-cesium/browser.js | 1 + Tools/eslint-config-cesium/index.js | 2 +- Tools/eslint-config-cesium/package.json | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 88528b77f41..5559ce5b6e8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": "cesium/browser" + "extends": "./Tools/eslint-config-cesium/browser.js", + "rules": { + "no-unused-vars": ["error", {"vars": "all", "args": "none"}] + } } diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js index 1b9a8440f35..4c49235ff7f 100644 --- a/Tools/eslint-config-cesium/browser.js +++ b/Tools/eslint-config-cesium/browser.js @@ -3,6 +3,7 @@ module.exports = { extends: 'cesium', env: { + amd: true, browser: true }, plugins: [ diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js index ee6e5943c71..18b52b76e97 100644 --- a/Tools/eslint-config-cesium/index.js +++ b/Tools/eslint-config-cesium/index.js @@ -28,7 +28,7 @@ module.exports = { 'no-irregular-whitespace': ['error'], 'no-new': ['error'], 'no-undef': ['error'], - 'no-unused-vars': ['error', {vars: 'all', args: 'none'}], + 'no-unused-vars': ['error', {vars: 'all', args: 'all'}], semi: ['error'], strict: ['error'], 'wrap-iife': ['error', 'any'] diff --git a/Tools/eslint-config-cesium/package.json b/Tools/eslint-config-cesium/package.json index 8c55a82bf7b..a51549c3db6 100644 --- a/Tools/eslint-config-cesium/package.json +++ b/Tools/eslint-config-cesium/package.json @@ -2,6 +2,7 @@ "name": "eslint-config-cesium", "version": "0.1.0", "description": "ESLint shareable configs for Cesium", + "homepage": "http://cesiumjs.org", "license": "Apache-2.0", "author": { "name": "Analytical Graphics, Inc.", From 80851e88d0e1c65f7d277325fa98803ddb610c82 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Fri, 9 Jun 2017 10:23:15 -0400 Subject: [PATCH 38/41] Remove eslint-config-cesium dependency. Should pass travis now. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index f4cc29a11dc..ac2988c0a2a 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "compressible": "^2.0.9", "compression": "^1.6.2", "electron": "^1.6.1", - "eslint-config-cesium": "^0.1.0", "eslint-plugin-html": "^2.0.3", "event-stream": "^3.3.4", "express": "^4.15.0", From b86320b9219b66b7a57ddba1b5bb8131d57c1725 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Fri, 9 Jun 2017 10:36:12 -0400 Subject: [PATCH 39/41] Remove more eslint-config-cesium package dependencies --- Tools/eslint-config-cesium/.eslintrc.json | 2 +- Tools/eslint-config-cesium/browser.js | 2 +- Tools/eslint-config-cesium/node.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tools/eslint-config-cesium/.eslintrc.json b/Tools/eslint-config-cesium/.eslintrc.json index fcf4081f55c..eb0e708be52 100644 --- a/Tools/eslint-config-cesium/.eslintrc.json +++ b/Tools/eslint-config-cesium/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "cesium/node" + "extends": "./node.js" } diff --git a/Tools/eslint-config-cesium/browser.js b/Tools/eslint-config-cesium/browser.js index 4c49235ff7f..fc021d6350f 100644 --- a/Tools/eslint-config-cesium/browser.js +++ b/Tools/eslint-config-cesium/browser.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - extends: 'cesium', + extends: './index.js', env: { amd: true, browser: true diff --git a/Tools/eslint-config-cesium/node.js b/Tools/eslint-config-cesium/node.js index 84b1b007a1f..2686d2251d8 100644 --- a/Tools/eslint-config-cesium/node.js +++ b/Tools/eslint-config-cesium/node.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - extends: 'cesium', + extends: './index.js', env: { node: true } From 06b3aca5fe6175ef1001d258b8b17ce5342cad65 Mon Sep 17 00:00:00 2001 From: Ottavio Hartman Date: Fri, 9 Jun 2017 13:55:31 -0400 Subject: [PATCH 40/41] Update version/date in package.json. Add more to README --- Tools/eslint-config-cesium/CHANGES.md | 2 +- Tools/eslint-config-cesium/README.md | 30 +++++++++++++++++++++++++++++- Tools/eslint-config-cesium/package.json | 5 +++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Tools/eslint-config-cesium/CHANGES.md b/Tools/eslint-config-cesium/CHANGES.md index e0d3d1417c9..90f43196b3c 100644 --- a/Tools/eslint-config-cesium/CHANGES.md +++ b/Tools/eslint-config-cesium/CHANGES.md @@ -1,6 +1,6 @@ Change Log ========== -### 0.1.0 - 2017-06-06 +### 1.0.0 - 2017-06-09 * Initial release. diff --git a/Tools/eslint-config-cesium/README.md b/Tools/eslint-config-cesium/README.md index 93019f9e8f9..16ded425aaa 100644 --- a/Tools/eslint-config-cesium/README.md +++ b/Tools/eslint-config-cesium/README.md @@ -1 +1,29 @@ -A [shareable ESLint config](http://eslint.org/docs/developer-guide/shareable-configs) for [Cesium](https://cesiumjs.org/). +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/package.json b/Tools/eslint-config-cesium/package.json index a51549c3db6..57878d08690 100644 --- a/Tools/eslint-config-cesium/package.json +++ b/Tools/eslint-config-cesium/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-cesium", - "version": "0.1.0", + "version": "1.0.0", "description": "ESLint shareable configs for Cesium", "homepage": "http://cesiumjs.org", "license": "Apache-2.0", @@ -19,6 +19,7 @@ "cesium" ], "peerDependencies": { - "eslint": ">= 3" + "eslint": ">= 3", + "eslint-plugin-html": ">= 2" } } From f88720d22e488af0bd33727d5ca22f839b030e94 Mon Sep 17 00:00:00 2001 From: Matthew Amato Date: Mon, 12 Jun 2017 10:19:27 -0400 Subject: [PATCH 41/41] Update to eslint 4.0 and eslint-plugin-html 3.0 Disable `no-useless-escape` for now since we have lots of false positives. --- Tools/eslint-config-cesium/CHANGES.md | 2 +- Tools/eslint-config-cesium/index.js | 1 + Tools/eslint-config-cesium/package.json | 4 ++-- package.json | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Tools/eslint-config-cesium/CHANGES.md b/Tools/eslint-config-cesium/CHANGES.md index 90f43196b3c..e4de4fad870 100644 --- a/Tools/eslint-config-cesium/CHANGES.md +++ b/Tools/eslint-config-cesium/CHANGES.md @@ -1,6 +1,6 @@ Change Log ========== -### 1.0.0 - 2017-06-09 +### 1.0.0 - 2017-06-12 * Initial release. diff --git a/Tools/eslint-config-cesium/index.js b/Tools/eslint-config-cesium/index.js index 18b52b76e97..b3a82e9a376 100644 --- a/Tools/eslint-config-cesium/index.js +++ b/Tools/eslint-config-cesium/index.js @@ -29,6 +29,7 @@ module.exports = { '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/package.json b/Tools/eslint-config-cesium/package.json index 57878d08690..afa7027fbe8 100644 --- a/Tools/eslint-config-cesium/package.json +++ b/Tools/eslint-config-cesium/package.json @@ -19,7 +19,7 @@ "cesium" ], "peerDependencies": { - "eslint": ">= 3", - "eslint-plugin-html": ">= 2" + "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",