1433 lines
39 KiB
JavaScript
1433 lines
39 KiB
JavaScript
import AssociativeArray from "../Core/AssociativeArray.js";
|
|
import Cartesian3 from "../Core/Cartesian3.js";
|
|
import CesiumMath from "../Core/Math.js";
|
|
import defined from "../Core/defined.js";
|
|
import destroyObject from "../Core/destroyObject.js";
|
|
import DeveloperError from "../Core/DeveloperError.js";
|
|
import Entity from "./Entity.js";
|
|
import JulianDate from "../Core/JulianDate.js";
|
|
import Matrix3 from "../Core/Matrix3.js";
|
|
import Matrix4 from "../Core/Matrix4.js";
|
|
import ReferenceFrame from "../Core/ReferenceFrame.js";
|
|
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";
|
|
import PathMode from "./PathMode.js";
|
|
import Property from "./Property.js";
|
|
import ReferenceProperty from "./ReferenceProperty.js";
|
|
import SampledPositionProperty from "./SampledPositionProperty.js";
|
|
import ScaledPositionProperty from "./ScaledPositionProperty.js";
|
|
import TimeIntervalCollectionPositionProperty from "./TimeIntervalCollectionPositionProperty.js";
|
|
import Quaternion from "../Core/Quaternion.js";
|
|
import arrayRemoveDuplicates from "../Core/arrayRemoveDuplicates.js";
|
|
|
|
const update3DMatrix3Scratch1 = new Matrix3();
|
|
const update3DMatrix3Scratch2 = new Matrix3();
|
|
const update3DMatrix3Scratch3 = new Matrix3();
|
|
const update3DCartesian3Scratch0 = new Cartesian3();
|
|
const update3DCartesian3Scratch1 = new Cartesian3();
|
|
const update3DCartesian3Scratch2 = new Cartesian3();
|
|
const update3DCartesian3Scratch3 = new Cartesian3();
|
|
|
|
const transformOrientationScratch = new Quaternion();
|
|
const transformVvlhScratch = new Matrix4();
|
|
const transformRotationScratch = new Matrix3();
|
|
|
|
/**
|
|
* Transforms a path entity's position into the local frame of the reference entity.
|
|
* If the reference entity has an orientation, uses that orientation to define the local frame.
|
|
* Otherwise, falls back to a VVLH (Vehicle Velocity Local Horizontal) frame derived from the reference entity's velocity.
|
|
*
|
|
* @param {JulianDate} time The time at which to evaluate the orientation or VVLH frame.
|
|
* @param {Cartesian3} pathEntityPos The position of the path entity in the FIXED reference frame.
|
|
* @param {Cartesian3} refEntityPos The position of the reference entity in the FIXED reference frame.
|
|
* @param {Entity} refEntity The reference entity whose frame to transform into.
|
|
* @param {Cartesian3} result The object onto which to store the result.
|
|
* @returns {Cartesian3 | undefined} The transformed position in the reference entity's local frame, or undefined if either input position is undefined.
|
|
*/
|
|
function transformToEntityFrame(
|
|
time,
|
|
pathEntityPos,
|
|
refEntityPos,
|
|
refEntity,
|
|
result,
|
|
) {
|
|
if (!defined(pathEntityPos) || !defined(refEntityPos)) {
|
|
return undefined;
|
|
}
|
|
|
|
Cartesian3.subtract(pathEntityPos, refEntityPos, result);
|
|
if (defined(refEntity.orientation)) {
|
|
if (refEntity.orientation.getValue(time, transformOrientationScratch)) {
|
|
Quaternion.conjugate(
|
|
transformOrientationScratch,
|
|
transformOrientationScratch,
|
|
);
|
|
Matrix3.fromQuaternion(
|
|
transformOrientationScratch,
|
|
transformRotationScratch,
|
|
);
|
|
Matrix3.multiplyByVector(transformRotationScratch, result, result);
|
|
}
|
|
} else if (
|
|
defined(
|
|
computeVvlhTransform(time, refEntity.position, transformVvlhScratch),
|
|
)
|
|
) {
|
|
Matrix4.inverse(transformVvlhScratch, transformVvlhScratch);
|
|
Matrix4.getRotation(transformVvlhScratch, transformRotationScratch);
|
|
Matrix3.multiplyByVector(transformRotationScratch, result, result);
|
|
} else {
|
|
// If neither ref entity's orientation nor VVLH are defined, return undefined
|
|
// This could happen if, for a given position of the entity we are drawing the path for,
|
|
// the ref entity doesn't have a position defined
|
|
return undefined;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Compute the vehicle velocity, local horizontal (VVLH) transform for a position property at a given time.
|
|
* The VVLH axes is defined based on the motion of the provided position point as follows:
|
|
* - The X axis is directed toward the point's velocity vector, in the direction of motion.
|
|
* - The Y axis is along the angular momentum vector.
|
|
* - The Z axis is along the position vector.
|
|
*
|
|
* @param {JulianDate} time The time at which to compute the VVLH transform.
|
|
* @param {PositionProperty} positionProperty The position to compute the VVLH frame for.
|
|
* @param {Matrix4} result The object onto which to store the result.
|
|
* @returns {Matrix4} The VVLH transform.
|
|
*/
|
|
function computeVvlhTransform(time, positionProperty, result) {
|
|
const cartesian = positionProperty.getValue(time, update3DCartesian3Scratch0);
|
|
if (defined(cartesian)) {
|
|
// The time delta was determined based on how fast satellites move compared to vehicles near the surface.
|
|
// Slower moving vehicles will most likely default to east-north-up, while faster ones will be LVLH.
|
|
const deltaTime = JulianDate.addSeconds(time, 0.01, new JulianDate());
|
|
const deltaCartesian = positionProperty.getValue(
|
|
deltaTime,
|
|
update3DCartesian3Scratch1,
|
|
);
|
|
if (
|
|
defined(deltaCartesian) &&
|
|
!Cartesian3.equalsEpsilon(cartesian, deltaCartesian, CesiumMath.EPSILON16)
|
|
) {
|
|
let toInertial = Transforms.computeFixedToIcrfMatrix(
|
|
time,
|
|
update3DMatrix3Scratch1,
|
|
);
|
|
let toInertialDelta = Transforms.computeFixedToIcrfMatrix(
|
|
deltaTime,
|
|
update3DMatrix3Scratch2,
|
|
);
|
|
let toFixed;
|
|
|
|
if (!defined(toInertial) || !defined(toInertialDelta)) {
|
|
toFixed = Transforms.computeTemeToPseudoFixedMatrix(
|
|
time,
|
|
update3DMatrix3Scratch3,
|
|
);
|
|
toInertial = Matrix3.transpose(toFixed, update3DMatrix3Scratch1);
|
|
toInertialDelta = Transforms.computeTemeToPseudoFixedMatrix(
|
|
deltaTime,
|
|
update3DMatrix3Scratch2,
|
|
);
|
|
Matrix3.transpose(toInertialDelta, toInertialDelta);
|
|
} else {
|
|
toFixed = Matrix3.transpose(toInertial, update3DMatrix3Scratch3);
|
|
}
|
|
|
|
// Z along the position
|
|
const zBasis = update3DCartesian3Scratch2;
|
|
Cartesian3.normalize(cartesian, zBasis);
|
|
Cartesian3.normalize(deltaCartesian, deltaCartesian);
|
|
|
|
Matrix3.multiplyByVector(toInertial, zBasis, zBasis);
|
|
Matrix3.multiplyByVector(toInertialDelta, deltaCartesian, deltaCartesian);
|
|
|
|
// Y is along the angular momentum vector (e.g. "orbit normal")
|
|
const yBasis = Cartesian3.cross(
|
|
zBasis,
|
|
deltaCartesian,
|
|
update3DCartesian3Scratch3,
|
|
);
|
|
if (
|
|
!Cartesian3.equalsEpsilon(yBasis, Cartesian3.ZERO, CesiumMath.EPSILON16)
|
|
) {
|
|
// X is along the cross of y and z (right handed basis / in the direction of motion)
|
|
const xBasis = Cartesian3.cross(
|
|
yBasis,
|
|
zBasis,
|
|
update3DCartesian3Scratch1,
|
|
);
|
|
|
|
Matrix3.multiplyByVector(toFixed, xBasis, xBasis);
|
|
Matrix3.multiplyByVector(toFixed, yBasis, yBasis);
|
|
Matrix3.multiplyByVector(toFixed, zBasis, zBasis);
|
|
|
|
Cartesian3.normalize(xBasis, xBasis);
|
|
Cartesian3.normalize(yBasis, yBasis);
|
|
Cartesian3.normalize(zBasis, zBasis);
|
|
|
|
if (!defined(result)) {
|
|
result = new Matrix4();
|
|
}
|
|
|
|
result[0] = xBasis.x;
|
|
result[1] = xBasis.y;
|
|
result[2] = xBasis.z;
|
|
result[3] = 0.0;
|
|
result[4] = yBasis.x;
|
|
result[5] = yBasis.y;
|
|
result[6] = yBasis.z;
|
|
result[7] = 0.0;
|
|
result[8] = zBasis.x;
|
|
result[9] = zBasis.y;
|
|
result[10] = zBasis.z;
|
|
result[11] = 0.0;
|
|
result[12] = cartesian.x;
|
|
result[13] = cartesian.y;
|
|
result[14] = cartesian.z;
|
|
result[15] = 1.0;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const defaultResolution = 60.0;
|
|
const defaultWidth = 1.0;
|
|
|
|
const scratchTimeInterval = new TimeInterval();
|
|
const subSampleCompositePropertyScratch = new TimeInterval();
|
|
const subSampleIntervalPropertyScratch = new TimeInterval();
|
|
|
|
function EntityData(entity) {
|
|
this.entity = entity;
|
|
this.polyline = undefined;
|
|
this.segmentPolylines = [];
|
|
this.index = undefined;
|
|
this.updater = undefined;
|
|
}
|
|
|
|
const sampleScratch = new Cartesian3();
|
|
function subSampleSampledProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
times,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
startingIndex,
|
|
result,
|
|
) {
|
|
let refEntity;
|
|
let refPosition;
|
|
|
|
let entityFrame = false;
|
|
if (referenceFrame instanceof Entity) {
|
|
refEntity = referenceFrame;
|
|
refPosition = refEntity.position;
|
|
referenceFrame = ReferenceFrame.FIXED;
|
|
entityFrame = true;
|
|
}
|
|
|
|
let r = startingIndex;
|
|
//Always step exactly on start (but only use it if it exists.)
|
|
let tmp;
|
|
let tmp2;
|
|
tmp = property.getValueInReferenceFrame(start, referenceFrame, result[r]);
|
|
if (!entityFrame) {
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
} else {
|
|
tmp2 = refPosition.getValueInReferenceFrame(
|
|
start,
|
|
referenceFrame,
|
|
sampleScratch,
|
|
);
|
|
|
|
// Transform to frame of reference - either reference entity's orientation, or VVLH
|
|
tmp = transformToEntityFrame(start, tmp, tmp2, refEntity, tmp);
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
}
|
|
|
|
let steppedOnNow =
|
|
!defined(updateTime) ||
|
|
JulianDate.lessThanOrEquals(updateTime, start) ||
|
|
JulianDate.greaterThanOrEquals(updateTime, stop);
|
|
|
|
//Iterate over all interval times and add the ones that fall in our
|
|
//time range. Note that times can contain data outside of
|
|
//the intervals range. This is by design for use with interpolation.
|
|
let t = 0;
|
|
const len = times.length;
|
|
let current = times[t];
|
|
const loopStop = stop;
|
|
let sampling = false;
|
|
let sampleStepsToTake;
|
|
let sampleStepsTaken;
|
|
let sampleStepSize;
|
|
|
|
while (t < len) {
|
|
if (!steppedOnNow && JulianDate.greaterThanOrEquals(current, updateTime)) {
|
|
tmp = property.getValueInReferenceFrame(
|
|
updateTime,
|
|
referenceFrame,
|
|
result[r],
|
|
);
|
|
if (!entityFrame) {
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
} else {
|
|
tmp2 = refPosition.getValueInReferenceFrame(
|
|
updateTime,
|
|
referenceFrame,
|
|
sampleScratch,
|
|
);
|
|
if (defined(tmp) && defined(tmp2)) {
|
|
tmp = transformToEntityFrame(updateTime, tmp, tmp2, refEntity, tmp);
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
}
|
|
}
|
|
steppedOnNow = true;
|
|
}
|
|
if (
|
|
JulianDate.greaterThan(current, start) &&
|
|
JulianDate.lessThan(current, loopStop) &&
|
|
!current.equals(updateTime)
|
|
) {
|
|
tmp = property.getValueInReferenceFrame(
|
|
current,
|
|
referenceFrame,
|
|
result[r],
|
|
);
|
|
if (!entityFrame) {
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
} else {
|
|
tmp2 = refPosition.getValueInReferenceFrame(
|
|
current,
|
|
referenceFrame,
|
|
sampleScratch,
|
|
);
|
|
if (defined(tmp) && defined(tmp2)) {
|
|
tmp = transformToEntityFrame(current, tmp, tmp2, refEntity, tmp);
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (t < len - 1) {
|
|
if (maximumStep > 0 && !sampling) {
|
|
const next = times[t + 1];
|
|
const secondsUntilNext = JulianDate.secondsDifference(next, current);
|
|
sampling = secondsUntilNext > maximumStep;
|
|
|
|
if (sampling) {
|
|
sampleStepsToTake = Math.ceil(secondsUntilNext / maximumStep);
|
|
sampleStepsTaken = 0;
|
|
sampleStepSize = secondsUntilNext / Math.max(sampleStepsToTake, 2);
|
|
sampleStepsToTake = Math.max(sampleStepsToTake - 1, 1);
|
|
}
|
|
}
|
|
|
|
if (sampling && sampleStepsTaken < sampleStepsToTake) {
|
|
current = JulianDate.addSeconds(
|
|
current,
|
|
sampleStepSize,
|
|
new JulianDate(),
|
|
);
|
|
sampleStepsTaken++;
|
|
continue;
|
|
}
|
|
}
|
|
sampling = false;
|
|
t++;
|
|
current = times[t];
|
|
}
|
|
|
|
//Always step exactly on stop (but only use it if it exists.)
|
|
tmp = property.getValueInReferenceFrame(stop, referenceFrame, result[r]);
|
|
if (!entityFrame) {
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
} else {
|
|
tmp2 = refPosition.getValueInReferenceFrame(
|
|
stop,
|
|
referenceFrame,
|
|
sampleScratch,
|
|
);
|
|
if (defined(tmp) && defined(tmp2)) {
|
|
tmp = transformToEntityFrame(stop, tmp, tmp2, refEntity, tmp);
|
|
if (defined(tmp)) {
|
|
result[r++] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
startingIndex,
|
|
result,
|
|
) {
|
|
let tmp;
|
|
let i = 0;
|
|
let index = startingIndex;
|
|
let time = start;
|
|
const stepSize = Math.max(maximumStep, 60);
|
|
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, stepSize * i, new JulianDate());
|
|
}
|
|
//Always sample stop.
|
|
tmp = property.getValueInReferenceFrame(stop, referenceFrame, result[index]);
|
|
if (defined(tmp)) {
|
|
result[index] = tmp;
|
|
index++;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
function subSampleIntervalProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
startingIndex,
|
|
result,
|
|
) {
|
|
subSampleIntervalPropertyScratch.start = start;
|
|
subSampleIntervalPropertyScratch.stop = stop;
|
|
|
|
let index = startingIndex;
|
|
const intervals = property.intervals;
|
|
for (let i = 0; i < intervals.length; i++) {
|
|
const interval = intervals.get(i);
|
|
if (
|
|
!TimeInterval.intersect(
|
|
interval,
|
|
subSampleIntervalPropertyScratch,
|
|
scratchTimeInterval,
|
|
).isEmpty
|
|
) {
|
|
let time = interval.start;
|
|
if (!interval.isStartIncluded) {
|
|
if (interval.isStopIncluded) {
|
|
time = interval.stop;
|
|
} else {
|
|
time = JulianDate.addSeconds(
|
|
interval.start,
|
|
JulianDate.secondsDifference(interval.stop, interval.start) / 2,
|
|
new JulianDate(),
|
|
);
|
|
}
|
|
}
|
|
const tmp = property.getValueInReferenceFrame(
|
|
time,
|
|
referenceFrame,
|
|
result[index],
|
|
);
|
|
if (defined(tmp)) {
|
|
result[index] = tmp;
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
function subSampleConstantProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
startingIndex,
|
|
result,
|
|
) {
|
|
const tmp = property.getValueInReferenceFrame(
|
|
start,
|
|
referenceFrame,
|
|
result[startingIndex],
|
|
);
|
|
if (defined(tmp)) {
|
|
result[startingIndex++] = tmp;
|
|
}
|
|
return startingIndex;
|
|
}
|
|
|
|
function subSampleCompositeProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
startingIndex,
|
|
result,
|
|
) {
|
|
subSampleCompositePropertyScratch.start = start;
|
|
subSampleCompositePropertyScratch.stop = stop;
|
|
|
|
let index = startingIndex;
|
|
const intervals = property.intervals;
|
|
for (let i = 0; i < intervals.length; i++) {
|
|
const interval = intervals.get(i);
|
|
if (
|
|
!TimeInterval.intersect(
|
|
interval,
|
|
subSampleCompositePropertyScratch,
|
|
scratchTimeInterval,
|
|
).isEmpty
|
|
) {
|
|
const intervalStart = interval.start;
|
|
const intervalStop = interval.stop;
|
|
|
|
let sampleStart = start;
|
|
if (JulianDate.greaterThan(intervalStart, sampleStart)) {
|
|
sampleStart = intervalStart;
|
|
}
|
|
|
|
let sampleStop = stop;
|
|
if (JulianDate.lessThan(intervalStop, sampleStop)) {
|
|
sampleStop = intervalStop;
|
|
}
|
|
|
|
index = reallySubSample(
|
|
interval.data,
|
|
sampleStart,
|
|
sampleStop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
index,
|
|
result,
|
|
);
|
|
}
|
|
}
|
|
return index;
|
|
}
|
|
|
|
function reallySubSample(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
index,
|
|
result,
|
|
) {
|
|
//Unwrap any references until we have the actual property.
|
|
while (property instanceof ReferenceProperty) {
|
|
property = property.resolvedProperty;
|
|
}
|
|
|
|
if (property instanceof SampledPositionProperty) {
|
|
const times = property._property._times;
|
|
index = subSampleSampledProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
times,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
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,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
index,
|
|
result,
|
|
);
|
|
} else if (property instanceof TimeIntervalCollectionPositionProperty) {
|
|
index = subSampleIntervalProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
index,
|
|
result,
|
|
);
|
|
} else if (
|
|
property instanceof ConstantPositionProperty ||
|
|
(property instanceof ScaledPositionProperty &&
|
|
Property.isConstant(property))
|
|
) {
|
|
index = subSampleConstantProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
index,
|
|
result,
|
|
);
|
|
} else {
|
|
//Fallback to generic sampling.
|
|
index = subSampleGenericProperty(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
index,
|
|
result,
|
|
);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
function subSample(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
result,
|
|
) {
|
|
if (!defined(result)) {
|
|
result = [];
|
|
}
|
|
|
|
const length = reallySubSample(
|
|
property,
|
|
start,
|
|
stop,
|
|
updateTime,
|
|
referenceFrame,
|
|
maximumStep,
|
|
0,
|
|
result,
|
|
);
|
|
result.length = length;
|
|
return result;
|
|
}
|
|
|
|
const toFixedScratch = new Matrix3();
|
|
const updateOrientationScratch = new Quaternion();
|
|
const updateRotationScratch = new Matrix3();
|
|
const portionsVisibleIntervalScratch = new TimeInterval();
|
|
const portionsSegmentIntervalScratch = new TimeInterval();
|
|
const portionsDynamicIntervalScratch = new TimeInterval();
|
|
|
|
function getDynamicMaterialProperties(materialData) {
|
|
const dynamic = [];
|
|
if (!defined(materialData)) {
|
|
return dynamic;
|
|
}
|
|
|
|
for (const key in materialData) {
|
|
// Material properties store uniforms as underscored backing fields.
|
|
// Ignore event and subscription bookkeeping fields.
|
|
if (
|
|
key[0] !== "_" ||
|
|
key === "_definitionChanged" ||
|
|
key.endsWith("Subscription")
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const prop = materialData[key];
|
|
if (
|
|
defined(prop) &&
|
|
typeof prop.getValue === "function" &&
|
|
!Property.isConstant(prop)
|
|
) {
|
|
dynamic.push(prop);
|
|
}
|
|
}
|
|
|
|
return dynamic;
|
|
}
|
|
|
|
function emitSegmentsForSplitTimes(
|
|
splitTimes,
|
|
positionProperty,
|
|
time,
|
|
pathGraphics,
|
|
entity,
|
|
item,
|
|
polylineCollection,
|
|
resolution,
|
|
referenceFrame,
|
|
materialProp,
|
|
startingSegIndex,
|
|
) {
|
|
// Sort and dedupe
|
|
splitTimes.sort(JulianDate.compare);
|
|
splitTimes = arrayRemoveDuplicates(splitTimes, JulianDate.equalsEpsilon);
|
|
|
|
let segIndex = startingSegIndex;
|
|
for (let j = 0; j < splitTimes.length - 1; j++) {
|
|
const splitStart = splitTimes[j];
|
|
const splitStop = splitTimes[j + 1];
|
|
if (!JulianDate.lessThan(splitStart, splitStop)) {
|
|
continue;
|
|
}
|
|
|
|
// Get segment midpoint
|
|
const splitMidTime = JulianDate.addSeconds(
|
|
splitStart,
|
|
JulianDate.secondsDifference(splitStop, splitStart) / 2,
|
|
new JulianDate(),
|
|
);
|
|
|
|
// Subsample positions
|
|
const subPositions = subSample(
|
|
positionProperty,
|
|
splitStart,
|
|
splitStop,
|
|
time,
|
|
referenceFrame,
|
|
resolution,
|
|
[],
|
|
);
|
|
if (subPositions.length < 2) {
|
|
continue;
|
|
}
|
|
|
|
// Get or create a polyline for this segment
|
|
let segPolyline = item.segmentPolylines[segIndex];
|
|
if (!defined(segPolyline)) {
|
|
segPolyline = polylineCollection.add();
|
|
segPolyline.id = entity;
|
|
item.segmentPolylines[segIndex] = segPolyline;
|
|
}
|
|
segPolyline.show = true;
|
|
segPolyline.positions = subPositions;
|
|
segPolyline.material = MaterialProperty.getValue(
|
|
splitMidTime,
|
|
materialProp,
|
|
segPolyline.material,
|
|
);
|
|
segPolyline.width = Property.getValueOrDefault(
|
|
pathGraphics._width,
|
|
time,
|
|
defaultWidth,
|
|
);
|
|
|
|
segIndex++;
|
|
}
|
|
return segIndex;
|
|
}
|
|
|
|
function PolylineUpdater(scene, referenceFrame) {
|
|
this._unusedIndexes = [];
|
|
this._polylineCollection = new PolylineCollection();
|
|
this._scene = scene;
|
|
this._referenceFrame = referenceFrame;
|
|
scene.primitives.add(this._polylineCollection);
|
|
}
|
|
|
|
PolylineUpdater.prototype.update = function (time) {
|
|
const frame = this._referenceFrame;
|
|
if (frame === ReferenceFrame.INERTIAL) {
|
|
let toFixed = Transforms.computeIcrfToFixedMatrix(time, toFixedScratch);
|
|
if (!defined(toFixed)) {
|
|
toFixed = Transforms.computeTemeToPseudoFixedMatrix(time, toFixedScratch);
|
|
}
|
|
Matrix4.fromRotationTranslation(
|
|
toFixed,
|
|
Cartesian3.ZERO,
|
|
this._polylineCollection.modelMatrix,
|
|
);
|
|
} else if (frame instanceof Entity) {
|
|
const position = frame.position.getValue(time);
|
|
|
|
// Use the reference frame entity's orientation if it has one
|
|
if (defined(frame.orientation)) {
|
|
if (defined(frame.orientation.getValue(time, updateOrientationScratch))) {
|
|
// Calculate the model matrix that places the body-frame path points into the world
|
|
Matrix3.fromQuaternion(updateOrientationScratch, updateRotationScratch);
|
|
Matrix4.fromRotationTranslation(
|
|
updateRotationScratch,
|
|
position,
|
|
this._polylineCollection.modelMatrix,
|
|
);
|
|
}
|
|
} else {
|
|
computeVvlhTransform(
|
|
time,
|
|
frame.position,
|
|
this._polylineCollection.modelMatrix,
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
PolylineUpdater.prototype.updateObject = function (time, item) {
|
|
const entity = item.entity;
|
|
const pathGraphics = entity._path;
|
|
const positionProperty = entity._position;
|
|
|
|
let sampleStart;
|
|
let sampleStop;
|
|
const showProperty = pathGraphics._show;
|
|
let polyline = item.polyline;
|
|
let show =
|
|
entity.isShowing &&
|
|
entity.isAvailable(time) &&
|
|
(!defined(showProperty) || showProperty.getValue(time));
|
|
|
|
//While we want to show the path, there may not actually be anything to show
|
|
//depending on lead/trail settings. Compute the interval of the path to
|
|
//show and check against actual availability.
|
|
if (show) {
|
|
const leadTime = Property.getValueOrUndefined(pathGraphics._leadTime, time);
|
|
const trailTime = Property.getValueOrUndefined(
|
|
pathGraphics._trailTime,
|
|
time,
|
|
);
|
|
const availability = entity._availability;
|
|
const hasAvailability = defined(availability);
|
|
const hasLeadTime = defined(leadTime);
|
|
const hasTrailTime = defined(trailTime);
|
|
|
|
//Objects need to have either defined availability or both a lead and trail time in order to
|
|
//draw a path (since we can't draw "infinite" paths.
|
|
show = hasAvailability || (hasLeadTime && hasTrailTime);
|
|
|
|
//The final step is to compute the actual start/stop times of the path to show.
|
|
//If current time is outside of the availability interval, there's a chance that
|
|
//we won't have to draw anything anyway.
|
|
if (show) {
|
|
if (hasTrailTime) {
|
|
sampleStart = JulianDate.addSeconds(time, -trailTime, new JulianDate());
|
|
}
|
|
if (hasLeadTime) {
|
|
sampleStop = JulianDate.addSeconds(time, leadTime, new JulianDate());
|
|
}
|
|
|
|
if (hasAvailability) {
|
|
const start = availability.start;
|
|
const stop = availability.stop;
|
|
|
|
if (!hasTrailTime || JulianDate.greaterThan(start, sampleStart)) {
|
|
sampleStart = start;
|
|
}
|
|
|
|
if (!hasLeadTime || JulianDate.lessThan(stop, sampleStop)) {
|
|
sampleStop = stop;
|
|
}
|
|
}
|
|
show = JulianDate.lessThan(sampleStart, sampleStop);
|
|
}
|
|
}
|
|
|
|
if (!show) {
|
|
//don't bother creating or updating anything else
|
|
if (defined(polyline)) {
|
|
this._unusedIndexes.push(item.index);
|
|
item.polyline = undefined;
|
|
polyline.show = false;
|
|
item.index = undefined;
|
|
}
|
|
for (let j = 0; j < item.segmentPolylines.length; j++) {
|
|
item.segmentPolylines[j].show = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!defined(polyline)) {
|
|
const unusedIndexes = this._unusedIndexes;
|
|
const length = unusedIndexes.length;
|
|
if (length > 0) {
|
|
const index = unusedIndexes.pop();
|
|
polyline = this._polylineCollection.get(index);
|
|
item.index = index;
|
|
} else {
|
|
item.index = this._polylineCollection.length;
|
|
polyline = this._polylineCollection.add();
|
|
}
|
|
polyline.id = entity;
|
|
item.polyline = polyline;
|
|
}
|
|
|
|
const resolution = Property.getValueOrDefault(
|
|
pathGraphics._resolution,
|
|
time,
|
|
defaultResolution,
|
|
);
|
|
|
|
const positions = subSample(
|
|
positionProperty,
|
|
sampleStart,
|
|
sampleStop,
|
|
time,
|
|
this._referenceFrame,
|
|
resolution,
|
|
polyline.positions.slice(),
|
|
);
|
|
|
|
// If the path only has one point, don't show it
|
|
// This can happen if the position is sampled at a time when it is only defined at a single point
|
|
if (positions.length < 2) {
|
|
polyline.show = false;
|
|
for (let j = 0; j < item.segmentPolylines.length; j++) {
|
|
item.segmentPolylines[j].show = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
polyline.show = true;
|
|
polyline.positions = positions;
|
|
|
|
polyline.material = MaterialProperty.getValue(
|
|
time,
|
|
pathGraphics.material,
|
|
polyline.material,
|
|
);
|
|
|
|
const materialMode = Property.getValueOrUndefined(
|
|
pathGraphics.materialMode,
|
|
time,
|
|
);
|
|
const materialProp = pathGraphics.material;
|
|
if (materialMode === PathMode.PORTIONS && !materialProp.isConstant) {
|
|
// Hide the single polyline if it exists
|
|
if (defined(polyline)) {
|
|
polyline.show = false;
|
|
}
|
|
|
|
// Prevent non-positive split steps from creating non-terminating loops.
|
|
// Positive fractional resolutions are valid; only values <= 0 fall back to the default.
|
|
const splitResolution = resolution > 0 ? resolution : defaultResolution;
|
|
|
|
const intervals = materialProp.intervals;
|
|
let nextSegIndex = 0;
|
|
|
|
if (!defined(intervals)) {
|
|
// Sampled/interpolated root material - generate synthetic times at resolution intervals
|
|
const splitTimes = [
|
|
JulianDate.clone(sampleStart),
|
|
JulianDate.clone(sampleStop),
|
|
];
|
|
let splitTime = JulianDate.addSeconds(
|
|
sampleStart,
|
|
splitResolution,
|
|
new JulianDate(),
|
|
);
|
|
while (JulianDate.lessThan(splitTime, sampleStop)) {
|
|
splitTimes.push(JulianDate.clone(splitTime));
|
|
splitTime = JulianDate.addSeconds(
|
|
splitTime,
|
|
splitResolution,
|
|
new JulianDate(),
|
|
);
|
|
}
|
|
nextSegIndex = emitSegmentsForSplitTimes(
|
|
splitTimes,
|
|
positionProperty,
|
|
time,
|
|
pathGraphics,
|
|
entity,
|
|
item,
|
|
this._polylineCollection,
|
|
splitResolution,
|
|
this._referenceFrame,
|
|
pathGraphics.material,
|
|
nextSegIndex,
|
|
);
|
|
} else {
|
|
portionsVisibleIntervalScratch.start = sampleStart;
|
|
portionsVisibleIntervalScratch.stop = sampleStop;
|
|
portionsVisibleIntervalScratch.isStartIncluded = true;
|
|
portionsVisibleIntervalScratch.isStopIncluded = true;
|
|
|
|
// Interval-based material - process each interval separately
|
|
for (let i = 0; i < intervals.length; i++) {
|
|
const interval = intervals.get(i);
|
|
|
|
if (
|
|
!TimeInterval.intersect(
|
|
interval,
|
|
portionsVisibleIntervalScratch,
|
|
portionsSegmentIntervalScratch,
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const segStart = portionsSegmentIntervalScratch.start;
|
|
const segStop = portionsSegmentIntervalScratch.stop;
|
|
|
|
if (JulianDate.greaterThanOrEquals(segStart, segStop)) {
|
|
continue;
|
|
}
|
|
|
|
// Detect dynamic properties and collect their split times
|
|
const dynamic = getDynamicMaterialProperties(interval.data);
|
|
const splitTimes = [
|
|
JulianDate.clone(segStart),
|
|
JulianDate.clone(segStop),
|
|
];
|
|
|
|
for (let j = 0; j < dynamic.length; j++) {
|
|
const prop = dynamic[j];
|
|
const timeDynamicIntervals = prop.intervals;
|
|
|
|
if (defined(timeDynamicIntervals)) {
|
|
// Interval-based property: collect interval boundaries
|
|
for (let k = 0; k < timeDynamicIntervals.length; k++) {
|
|
const timeDynamicInterval = timeDynamicIntervals.get(k);
|
|
|
|
if (
|
|
!TimeInterval.intersect(
|
|
timeDynamicInterval,
|
|
portionsSegmentIntervalScratch,
|
|
portionsDynamicIntervalScratch,
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
JulianDate.greaterThan(
|
|
portionsDynamicIntervalScratch.start,
|
|
segStart,
|
|
) &&
|
|
JulianDate.lessThan(
|
|
portionsDynamicIntervalScratch.start,
|
|
segStop,
|
|
)
|
|
) {
|
|
splitTimes.push(
|
|
JulianDate.clone(portionsDynamicIntervalScratch.start),
|
|
);
|
|
}
|
|
|
|
if (
|
|
JulianDate.greaterThan(
|
|
portionsDynamicIntervalScratch.stop,
|
|
segStart,
|
|
) &&
|
|
JulianDate.lessThan(
|
|
portionsDynamicIntervalScratch.stop,
|
|
segStop,
|
|
)
|
|
) {
|
|
splitTimes.push(
|
|
JulianDate.clone(portionsDynamicIntervalScratch.stop),
|
|
);
|
|
}
|
|
}
|
|
} else if (!Property.isConstant(prop)) {
|
|
// Sampled/interpolated property: add resolution-based split times
|
|
let sampledTime = JulianDate.clone(segStart);
|
|
while (JulianDate.lessThan(sampledTime, segStop)) {
|
|
splitTimes.push(JulianDate.clone(sampledTime));
|
|
sampledTime = JulianDate.addSeconds(
|
|
sampledTime,
|
|
splitResolution,
|
|
new JulianDate(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit segments for this interval's split times
|
|
nextSegIndex = emitSegmentsForSplitTimes(
|
|
splitTimes,
|
|
positionProperty,
|
|
time,
|
|
pathGraphics,
|
|
entity,
|
|
item,
|
|
this._polylineCollection,
|
|
splitResolution,
|
|
this._referenceFrame,
|
|
interval.data,
|
|
nextSegIndex,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Hide any excess segment polylines from previous frames.
|
|
for (let j = nextSegIndex; j < item.segmentPolylines.length; j++) {
|
|
if (defined(item.segmentPolylines[j])) {
|
|
item.segmentPolylines[j].show = false;
|
|
}
|
|
}
|
|
} else {
|
|
// Not in PORTIONS mode, hide all segment polylines from previous frames
|
|
for (let j = 0; j < item.segmentPolylines.length; j++) {
|
|
item.segmentPolylines[j].show = false;
|
|
}
|
|
}
|
|
|
|
polyline.width = Property.getValueOrDefault(
|
|
pathGraphics._width,
|
|
time,
|
|
defaultWidth,
|
|
);
|
|
polyline.distanceDisplayCondition = Property.getValueOrUndefined(
|
|
pathGraphics._distanceDisplayCondition,
|
|
time,
|
|
polyline.distanceDisplayCondition,
|
|
);
|
|
};
|
|
|
|
PolylineUpdater.prototype.removeObject = function (item) {
|
|
const polyline = item.polyline;
|
|
if (defined(polyline)) {
|
|
this._unusedIndexes.push(item.index);
|
|
item.polyline = undefined;
|
|
polyline.show = false;
|
|
polyline.id = undefined;
|
|
item.index = undefined;
|
|
}
|
|
for (let i = 0; i < item.segmentPolylines.length; i++) {
|
|
item.segmentPolylines[i].show = false;
|
|
}
|
|
item.segmentPolylines.length = 0;
|
|
};
|
|
|
|
PolylineUpdater.prototype.destroy = function () {
|
|
this._scene.primitives.remove(this._polylineCollection);
|
|
return destroyObject(this);
|
|
};
|
|
|
|
/**
|
|
* A {@link Visualizer} which maps {@link Entity#path} to a {@link Polyline}.
|
|
* @alias PathVisualizer
|
|
* @constructor
|
|
*
|
|
* @param {Scene} scene The scene the primitives will be rendered in.
|
|
* @param {EntityCollection} entityCollection The entityCollection to visualize.
|
|
*/
|
|
function PathVisualizer(scene, entityCollection) {
|
|
//>>includeStart('debug', pragmas.debug);
|
|
if (!defined(scene)) {
|
|
throw new DeveloperError("scene is required.");
|
|
}
|
|
if (!defined(entityCollection)) {
|
|
throw new DeveloperError("entityCollection is required.");
|
|
}
|
|
//>>includeEnd('debug');
|
|
|
|
entityCollection.collectionChanged.addEventListener(
|
|
PathVisualizer.prototype._onCollectionChanged,
|
|
this,
|
|
);
|
|
|
|
this._scene = scene;
|
|
this._updaters = {};
|
|
this._entityCollection = entityCollection;
|
|
this._items = new AssociativeArray();
|
|
|
|
this._onCollectionChanged(entityCollection, entityCollection.values, [], []);
|
|
}
|
|
|
|
/**
|
|
* Updates all of the primitives created by this visualizer to match their
|
|
* Entity counterpart at the given time.
|
|
*
|
|
* @param {JulianDate} time The time to update to.
|
|
* @returns {boolean} This function always returns true.
|
|
*/
|
|
PathVisualizer.prototype.update = function (time) {
|
|
//>>includeStart('debug', pragmas.debug);
|
|
if (!defined(time)) {
|
|
throw new DeveloperError("time is required.");
|
|
}
|
|
//>>includeEnd('debug');
|
|
|
|
const updaters = this._updaters;
|
|
for (const key in updaters) {
|
|
if (updaters.hasOwnProperty(key)) {
|
|
updaters[key].update(time);
|
|
}
|
|
}
|
|
|
|
const items = this._items.values;
|
|
if (
|
|
items.length === 0 &&
|
|
defined(this._updaters) &&
|
|
Object.keys(this._updaters).length > 0
|
|
) {
|
|
for (const u in updaters) {
|
|
if (updaters.hasOwnProperty(u)) {
|
|
updaters[u].destroy();
|
|
}
|
|
}
|
|
this._updaters = {};
|
|
}
|
|
|
|
for (let i = 0, len = items.length; i < len; i++) {
|
|
const item = items[i];
|
|
const entity = item.entity;
|
|
const positionProperty = entity._position;
|
|
const pathGraphics = entity._path;
|
|
|
|
const lastUpdater = item.updater;
|
|
|
|
let isRelative = false;
|
|
|
|
let frameToVisualize = ReferenceFrame.FIXED;
|
|
let frameToVisualizeKey = frameToVisualize.toString();
|
|
if (this._scene.mode === SceneMode.SCENE3D) {
|
|
const relativeTo = Property.getValueOrUndefined(
|
|
pathGraphics.relativeTo,
|
|
time,
|
|
);
|
|
if (defined(relativeTo)) {
|
|
if (relativeTo === "FIXED") {
|
|
frameToVisualize = ReferenceFrame.FIXED;
|
|
frameToVisualizeKey = frameToVisualize.toString();
|
|
} else if (relativeTo === "INERTIAL") {
|
|
frameToVisualize = ReferenceFrame.INERTIAL;
|
|
frameToVisualizeKey = frameToVisualize.toString();
|
|
} else {
|
|
// Path should be relative to entity
|
|
// Current implementation uses VVLH, ignores entity orientation
|
|
isRelative = true;
|
|
frameToVisualize = this._entityCollection.getById(relativeTo);
|
|
frameToVisualizeKey = relativeTo;
|
|
}
|
|
} else {
|
|
frameToVisualize = positionProperty.referenceFrame;
|
|
frameToVisualizeKey = frameToVisualize.toString();
|
|
}
|
|
}
|
|
|
|
let currentUpdater = this._updaters[frameToVisualizeKey];
|
|
|
|
if (lastUpdater === currentUpdater && defined(currentUpdater)) {
|
|
currentUpdater.updateObject(time, item);
|
|
continue;
|
|
}
|
|
|
|
if (defined(lastUpdater)) {
|
|
lastUpdater.removeObject(item);
|
|
}
|
|
|
|
if (isRelative && !defined(frameToVisualize)) {
|
|
continue;
|
|
}
|
|
|
|
if (!defined(currentUpdater)) {
|
|
currentUpdater = new PolylineUpdater(this._scene, frameToVisualize);
|
|
currentUpdater.update(time);
|
|
this._updaters[frameToVisualizeKey] = currentUpdater;
|
|
}
|
|
|
|
item.updater = currentUpdater;
|
|
if (defined(currentUpdater)) {
|
|
currentUpdater.updateObject(time, item);
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Returns true if this object was destroyed; otherwise, false.
|
|
*
|
|
* @returns {boolean} True if this object was destroyed; otherwise, false.
|
|
*/
|
|
PathVisualizer.prototype.isDestroyed = function () {
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Removes and destroys all primitives created by this instance.
|
|
*/
|
|
PathVisualizer.prototype.destroy = function () {
|
|
this._entityCollection.collectionChanged.removeEventListener(
|
|
PathVisualizer.prototype._onCollectionChanged,
|
|
this,
|
|
);
|
|
|
|
const updaters = this._updaters;
|
|
for (const key in updaters) {
|
|
if (updaters.hasOwnProperty(key)) {
|
|
updaters[key].destroy();
|
|
}
|
|
}
|
|
|
|
return destroyObject(this);
|
|
};
|
|
|
|
PathVisualizer.prototype._onCollectionChanged = function (
|
|
entityCollection,
|
|
added,
|
|
removed,
|
|
changed,
|
|
) {
|
|
let i;
|
|
let entity;
|
|
let item;
|
|
const items = this._items;
|
|
|
|
for (i = added.length - 1; i > -1; i--) {
|
|
entity = added[i];
|
|
if (defined(entity._path) && defined(entity._position)) {
|
|
items.set(entity.id, new EntityData(entity));
|
|
}
|
|
}
|
|
|
|
for (i = changed.length - 1; i > -1; i--) {
|
|
entity = changed[i];
|
|
if (defined(entity._path) && defined(entity._position)) {
|
|
if (!items.contains(entity.id)) {
|
|
items.set(entity.id, new EntityData(entity));
|
|
}
|
|
} else {
|
|
item = items.get(entity.id);
|
|
if (defined(item)) {
|
|
if (defined(item.updater)) {
|
|
item.updater.removeObject(item);
|
|
}
|
|
items.remove(entity.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = removed.length - 1; i > -1; i--) {
|
|
entity = removed[i];
|
|
item = items.get(entity.id);
|
|
if (defined(item)) {
|
|
if (defined(item.updater)) {
|
|
item.updater.removeObject(item);
|
|
}
|
|
items.remove(entity.id);
|
|
}
|
|
}
|
|
};
|
|
|
|
//for testing
|
|
PathVisualizer._subSample = subSample;
|
|
PathVisualizer._computeVvlhTransform = computeVvlhTransform;
|
|
PathVisualizer._transformToEntityFrame = transformToEntityFrame;
|
|
export default PathVisualizer;
|