diff --git a/Apps/Sandcastle/gallery/Callback Position Property.html b/Apps/Sandcastle/gallery/Callback Position Property.html new file mode 100644 index 00000000000..19dab3dadca --- /dev/null +++ b/Apps/Sandcastle/gallery/Callback Position Property.html @@ -0,0 +1,221 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Callback Position Property.jpg b/Apps/Sandcastle/gallery/Callback Position Property.jpg new file mode 100644 index 00000000000..3a4c588906f Binary files /dev/null and b/Apps/Sandcastle/gallery/Callback Position Property.jpg differ diff --git a/CHANGES.md b/CHANGES.md index a21df090547..dc28e43893d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ ##### Additions :tada: - Added `enableVerticalExaggeration` option to models. Set this value to `false` to prevent model exaggeration when `Scene.verticalExaggeration` is set to a value other than `1.0`. [#12141](https://github.com/CesiumGS/cesium/pull/12141) +- Added `CallbackPositionProperty` to allow lazy entity position evaluation. [#12170](https://github.com/CesiumGS/cesium/pull/12170) ##### Fixes :wrench: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 396eb758ea8..4dcf359ec12 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -408,5 +408,6 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu - [Levi Montgomery](https://github.com/Levi-Montgomery) - [Brandon Berisford](https://github.com/BeyondBelief96) - [Lawrence Owen](https://github.com/ljowen) -- [Adam Wirth](https://https://github.com/adamwirth) +- [Adam Wirth](https://github.com/adamwirth) - [Javier Sanchez](https://github.com/jvrjsanchez) +- [Jérôme Fayot](https://github.com/jfayot) diff --git a/packages/engine/Source/DataSources/CallbackPositionProperty.js b/packages/engine/Source/DataSources/CallbackPositionProperty.js new file mode 100644 index 00000000000..61909f99566 --- /dev/null +++ b/packages/engine/Source/DataSources/CallbackPositionProperty.js @@ -0,0 +1,173 @@ +import defaultValue from "../Core/defaultValue.js"; +import defined from "../Core/defined.js"; +import DeveloperError from "../Core/DeveloperError.js"; +import Event from "../Core/Event.js"; +import JulianDate from "../Core/JulianDate.js"; +import ReferenceFrame from "../Core/ReferenceFrame.js"; +import PositionProperty from "./PositionProperty.js"; + +/** + * A {@link PositionProperty} whose value is lazily evaluated by a callback function. + * + * @alias CallbackPositionProperty + * @constructor + * + * @param {CallbackPositionProperty.Callback} callback The function to be called when the position property is evaluated. + * @param {boolean} isConstant true when the callback function returns the same value every time, false if the value will change. + * @param {ReferenceFrame} [referenceFrame=ReferenceFrame.FIXED] The reference frame in which the position is defined. + * + * @demo {@link https://sandcastle.cesium.com/index.html?src=Callback%20Position%20Property.html|Cesium Sandcastle Callback Position Property Demo} + */ +function CallbackPositionProperty(callback, isConstant, referenceFrame) { + this._callback = undefined; + this._isConstant = undefined; + this._referenceFrame = defaultValue(referenceFrame, ReferenceFrame.FIXED); + this._definitionChanged = new Event(); + this.setCallback(callback, isConstant); +} + +Object.defineProperties(CallbackPositionProperty.prototype, { + /** + * Gets a value indicating if this property is constant. + * @memberof CallbackPositionProperty.prototype + * + * @type {boolean} + * @readonly + */ + isConstant: { + get: function () { + return this._isConstant; + }, + }, + /** + * Gets the event that is raised whenever the definition of this property changes. + * The definition is considered to have changed if a call to getValue would return + * a different result for the same time. + * @memberof CallbackPositionProperty.prototype + * + * @type {Event} + * @readonly + */ + definitionChanged: { + get: function () { + return this._definitionChanged; + }, + }, + /** + * Gets the reference frame in which the position is defined. + * @memberof CallbackPositionProperty.prototype + * @type {ReferenceFrame} + * @default ReferenceFrame.FIXED; + */ + referenceFrame: { + get: function () { + return this._referenceFrame; + }, + }, +}); + +const timeScratch = new JulianDate(); + +/** + * Gets the value of the property at the provided time in the fixed frame. + * + * @param {JulianDate} [time=JulianDate.now()] The time for which to retrieve the value. If omitted, the current system time is used. + * @param {Cartesian3} [result] The object to store the value into, if omitted, a new instance is created and returned. + * @returns {Cartesian3 | undefined} The modified result parameter or a new instance if the result parameter was not supplied. + */ +CallbackPositionProperty.prototype.getValue = function (time, result) { + if (!defined(time)) { + time = JulianDate.now(timeScratch); + } + return this.getValueInReferenceFrame(time, ReferenceFrame.FIXED, result); +}; + +/** + * Sets the callback to be used. + * + * @param {CallbackPositionProperty.Callback} callback The function to be called when the property is evaluated. + * @param {boolean} isConstant true when the callback function returns the same value every time, false if the value will change. + */ +CallbackPositionProperty.prototype.setCallback = function ( + callback, + isConstant +) { + //>>includeStart('debug', pragmas.debug); + if (!defined(callback)) { + throw new DeveloperError("callback is required."); + } + if (!defined(isConstant)) { + throw new DeveloperError("isConstant is required."); + } + //>>includeEnd('debug'); + + const changed = + this._callback !== callback || this._isConstant !== isConstant; + + this._callback = callback; + this._isConstant = isConstant; + + if (changed) { + this._definitionChanged.raiseEvent(this); + } +}; + +/** + * Gets the value of the property at the provided time and in the provided reference frame. + * + * @param {JulianDate} time The time for which to retrieve the value. + * @param {ReferenceFrame} referenceFrame The desired referenceFrame of the result. + * @param {Cartesian3} [result] The object to store the value into, if omitted, a new instance is created and returned. + * @returns {Cartesian3 | undefined} The modified result parameter or a new instance if the result parameter was not supplied. + */ +CallbackPositionProperty.prototype.getValueInReferenceFrame = function ( + time, + referenceFrame, + result +) { + //>>includeStart('debug', pragmas.debug); + if (!defined(time)) { + throw new DeveloperError("time is required."); + } + if (!defined(referenceFrame)) { + throw new DeveloperError("referenceFrame is required."); + } + //>>includeEnd('debug'); + + const value = this._callback(time, result); + + return PositionProperty.convertToReferenceFrame( + time, + value, + this._referenceFrame, + referenceFrame, + result + ); +}; + +/** + * Compares this property to the provided property and returns + * true if they are equal, false otherwise. + * + * @param {Property} [other] The other property. + * @returns {boolean} true if left and right are equal, false otherwise. + */ +CallbackPositionProperty.prototype.equals = function (other) { + return ( + this === other || + (other instanceof CallbackPositionProperty && + this._callback === other._callback && + this._isConstant === other._isConstant && + this._referenceFrame === other._referenceFrame) + ); +}; + +/** + * A function that returns the value of the position property. + * @callback CallbackPositionProperty.Callback + * + * @param {JulianDate} [time=JulianDate.now()] The time for which to retrieve the value. If omitted, the current system time is used. + * @param {Cartesian3} [result] The object to store the value into. If omitted, the function must create and return a new instance. + * @returns {Cartesian3 | undefined} The modified result parameter, or a new instance if the result parameter was not supplied or is unsupported. + */ +export default CallbackPositionProperty; diff --git a/packages/engine/Source/DataSources/PathVisualizer.js b/packages/engine/Source/DataSources/PathVisualizer.js index 8ae9a415fff..6c99fc36815 100644 --- a/packages/engine/Source/DataSources/PathVisualizer.js +++ b/packages/engine/Source/DataSources/PathVisualizer.js @@ -11,6 +11,7 @@ import TimeInterval from "../Core/TimeInterval.js"; import Transforms from "../Core/Transforms.js"; import PolylineCollection from "../Scene/PolylineCollection.js"; import SceneMode from "../Scene/SceneMode.js"; +import CallbackPositionProperty from "./CallbackPositionProperty.js"; import CompositePositionProperty from "./CompositePositionProperty.js"; import ConstantPositionProperty from "./ConstantPositionProperty.js"; import MaterialProperty from "./MaterialProperty.js"; @@ -135,6 +136,58 @@ function subSampleSampledProperty( return r; } +function subSampleCallbackPositionProperty( + property, + start, + stop, + updateTime, + referenceFrame, + maximumStep, + startingIndex, + result +) { + let tmp; + let i = 0; + let index = startingIndex; + let time = start; + let steppedOnNow = + !defined(updateTime) || + JulianDate.lessThanOrEquals(updateTime, start) || + JulianDate.greaterThanOrEquals(updateTime, stop); + while (JulianDate.lessThan(time, stop)) { + if (!steppedOnNow && JulianDate.greaterThanOrEquals(time, updateTime)) { + steppedOnNow = true; + tmp = property.getValueInReferenceFrame( + updateTime, + referenceFrame, + result[index] + ); + if (defined(tmp)) { + result[index] = tmp; + index++; + } + } + tmp = property.getValueInReferenceFrame( + time, + referenceFrame, + result[index] + ); + if (defined(tmp)) { + result[index] = tmp; + index++; + } + i++; + time = JulianDate.addSeconds(start, maximumStep * i, new JulianDate()); + } + //Always sample stop. + tmp = property.getValueInReferenceFrame(stop, referenceFrame, result[index]); + if (defined(tmp)) { + result[index] = tmp; + index++; + } + return index; +} + function subSampleGenericProperty( property, start, @@ -339,6 +392,17 @@ function reallySubSample( index, result ); + } else if (property instanceof CallbackPositionProperty) { + index = subSampleCallbackPositionProperty( + property, + start, + stop, + updateTime, + referenceFrame, + maximumStep, + index, + result + ); } else if (property instanceof CompositePositionProperty) { index = subSampleCompositeProperty( property, diff --git a/packages/engine/Source/DataSources/PositionProperty.js b/packages/engine/Source/DataSources/PositionProperty.js index 66a9c384b67..17bce66ed19 100644 --- a/packages/engine/Source/DataSources/PositionProperty.js +++ b/packages/engine/Source/DataSources/PositionProperty.js @@ -14,6 +14,7 @@ import Transforms from "../Core/Transforms.js"; * @constructor * @abstract * + * @see CallbackPositionProperty * @see CompositePositionProperty * @see ConstantPositionProperty * @see SampledPositionProperty diff --git a/packages/engine/Specs/DataSources/CallbackPositionPropertySpec.js b/packages/engine/Specs/DataSources/CallbackPositionPropertySpec.js new file mode 100644 index 00000000000..c9c2470f89a --- /dev/null +++ b/packages/engine/Specs/DataSources/CallbackPositionPropertySpec.js @@ -0,0 +1,196 @@ +import { + JulianDate, + CallbackPositionProperty, + Cartesian3, + PositionProperty, + ReferenceFrame, +} from "../../index.js"; + +describe("DataSources/CallbackPositionProperty", function () { + const time = JulianDate.now(); + + it("constructor throws with undefined isConstant", function () { + expect(function () { + return new CallbackPositionProperty(function () {}, undefined); + }).toThrowDeveloperError(); + }); + + it("constructor throws with undefined callback", function () { + expect(function () { + return new CallbackPositionProperty(undefined, true); + }).toThrowDeveloperError(); + }); + + it("constructor sets expected defaults", function () { + const callback = jasmine.createSpy("callback"); + let property = new CallbackPositionProperty(callback, true); + expect(property.referenceFrame).toBe(ReferenceFrame.FIXED); + + property = new CallbackPositionProperty( + callback, + true, + ReferenceFrame.INERTIAL + ); + expect(property.referenceFrame).toBe(ReferenceFrame.INERTIAL); + }); + + it("callback received proper parameters", function () { + const result = {}; + const callback = jasmine.createSpy("callback"); + const property = new CallbackPositionProperty(callback, true); + property.getValue(time, result); + expect(callback).toHaveBeenCalledWith(time, result); + }); + + it("getValue returns callback result", function () { + const value = new Cartesian3(1, 2, 3); + const callback = function (_time, result) { + return value.clone(result); + }; + const property = new CallbackPositionProperty(callback, true); + const result = property.getValue(time); + expect(result).not.toBe(value); + expect(result).toEqual(value); + + const value2 = new Cartesian3(); + expect(property.getValue(time, value2)).toBe(value2); + }); + + it("getValue returns in fixed frame", function () { + const valueInertial = new Cartesian3(1, 2, 3); + const valueFixed = PositionProperty.convertToReferenceFrame( + time, + valueInertial, + ReferenceFrame.INERTIAL, + ReferenceFrame.FIXED + ); + const callback = function (_time, result) { + return valueInertial.clone(result); + }; + const property = new CallbackPositionProperty( + callback, + true, + ReferenceFrame.INERTIAL + ); + + const result = property.getValue(time); + expect(result).toEqual(valueFixed); + }); + + it("getValue uses JulianDate.now() if time parameter is undefined", function () { + spyOn(JulianDate, "now").and.callThrough(); + + const value = new Cartesian3(1, 2, 3); + const callback = function (_time, result) { + return value.clone(result); + }; + const property = new CallbackPositionProperty(callback, true); + const actualResult = property.getValue(); + expect(JulianDate.now).toHaveBeenCalled(); + expect(actualResult).toEqual(value); + }); + + it("getValueInReferenceFrame works without a result parameter", function () { + const value = new Cartesian3(1, 2, 3); + const callback = function (_time, result) { + return value.clone(result); + }; + const property = new CallbackPositionProperty(callback, true); + + const result = property.getValueInReferenceFrame( + time, + ReferenceFrame.INERTIAL + ); + expect(result).not.toBe(value); + expect(result).toEqual( + PositionProperty.convertToReferenceFrame( + time, + value, + ReferenceFrame.FIXED, + ReferenceFrame.INERTIAL + ) + ); + }); + + it("getValueInReferenceFrame works with a result parameter", function () { + const value = new Cartesian3(1, 2, 3); + const callback = function (_time, result) { + return value.clone(result); + }; + const property = new CallbackPositionProperty( + callback, + true, + ReferenceFrame.INERTIAL + ); + + const expected = new Cartesian3(); + const result = property.getValueInReferenceFrame( + time, + ReferenceFrame.FIXED, + expected + ); + expect(result).toBe(expected); + expect(expected).toEqual( + PositionProperty.convertToReferenceFrame( + time, + value, + ReferenceFrame.INERTIAL, + ReferenceFrame.FIXED + ) + ); + }); + + it("getValueInReferenceFrame throws with undefined time", function () { + const property = new CallbackPositionProperty(function () {}, true); + + expect(function () { + property.getValueInReferenceFrame(undefined, ReferenceFrame.FIXED); + }).toThrowDeveloperError(); + }); + + it("getValueInReferenceFrame throws with undefined reference frame", function () { + const property = new CallbackPositionProperty(function () {}, true); + + expect(function () { + property.getValueInReferenceFrame(time, undefined); + }).toThrowDeveloperError(); + }); + + it("isConstant returns correct value", function () { + const property = new CallbackPositionProperty(function () {}, true); + expect(property.isConstant).toBe(true); + property.setCallback(function () {}, false); + expect(property.isConstant).toBe(false); + }); + + it("setCallback raises definitionChanged event", function () { + const property = new CallbackPositionProperty(function () {}, true); + const listener = jasmine.createSpy("listener"); + property.definitionChanged.addEventListener(listener); + property.setCallback(function () {}, false); + expect(listener).toHaveBeenCalledWith(property); + }); + + it("equals works", function () { + const callback = function () {}; + const left = new CallbackPositionProperty(callback, true); + let right = new CallbackPositionProperty(callback, true); + + expect(left.equals(right)).toEqual(true); + + right.setCallback(callback, false); + expect(left.equals(right)).toEqual(false); + + right.setCallback(function () { + return undefined; + }, true); + expect(left.equals(right)).toEqual(false); + + right = new CallbackPositionProperty( + callback, + true, + ReferenceFrame.INERTIAL + ); + expect(left.equals(right)).toEqual(false); + }); +}); diff --git a/packages/engine/Specs/DataSources/ConstantPositionPropertySpec.js b/packages/engine/Specs/DataSources/ConstantPositionPropertySpec.js index 7c1442cdef9..0d4c734aeef 100644 --- a/packages/engine/Specs/DataSources/ConstantPositionPropertySpec.js +++ b/packages/engine/Specs/DataSources/ConstantPositionPropertySpec.js @@ -61,7 +61,7 @@ describe("DataSources/ConstantPositionProperty", function () { expect(property.getValue(time)).toBeUndefined(); }); - it("getValue work swith undefined inertial value", function () { + it("getValue works with undefined inertial value", function () { const property = new ConstantPositionProperty( undefined, ReferenceFrame.INERTIAL diff --git a/packages/engine/Specs/DataSources/PathVisualizerSpec.js b/packages/engine/Specs/DataSources/PathVisualizerSpec.js index c34f20ce89d..ce9ecfff278 100644 --- a/packages/engine/Specs/DataSources/PathVisualizerSpec.js +++ b/packages/engine/Specs/DataSources/PathVisualizerSpec.js @@ -16,6 +16,8 @@ import { PolylineOutlineMaterialProperty, ReferenceProperty, SampledPositionProperty, + CallbackPositionProperty, + LinearSpline, ScaledPositionProperty, TimeIntervalCollectionPositionProperty, SceneMode, @@ -733,6 +735,58 @@ describe( expect(result).toEqual([new Cartesian3(0, 0, 3)]); }); + it("subSample works for callback position properties", function () { + const t1 = new JulianDate(0, 0); + const t2 = new JulianDate(2, 0); + const updateTime = new JulianDate(1, 1); + const duration = JulianDate.secondsDifference(t2, t1); + const spline = new LinearSpline({ + times: [0, 1], + points: [new Cartesian3(0, 0, 0), new Cartesian3(0, 0, 1)], + }); + const callback = function (time, result) { + if (JulianDate.lessThan(time, t1) || JulianDate.greaterThan(time, t2)) { + return undefined; + } + if (result === undefined) { + result = new Cartesian3(); + } + const delta = JulianDate.secondsDifference(time, t1); + return spline.evaluate(delta / duration, result); + }; + + const property = new CallbackPositionProperty(callback, false); + + const referenceFrame = ReferenceFrame.FIXED; + const maximumStep = 43200; + const result = []; + PathVisualizer._subSample( + property, + t1, + t2, + updateTime, + referenceFrame, + maximumStep, + result + ); + expect(result).toEqual([ + property.getValue(t1), + property.getValue( + JulianDate.addSeconds(t1, maximumStep, new JulianDate()) + ), + property.getValue( + JulianDate.addSeconds(t1, maximumStep * 2, new JulianDate()) + ), + property.getValue(updateTime), + property.getValue( + JulianDate.addSeconds(t1, maximumStep * 3, new JulianDate()) + ), + property.getValue( + JulianDate.addSeconds(t1, maximumStep * 4, new JulianDate()) + ), + ]); + }); + function CustomPositionProperty(innerProperty) { this.SampledProperty = innerProperty; this.isConstant = innerProperty.isConstant;