Problem with entity that needs to "hold still" if using interpolated positions

I am using interpolation for my sampledPositionProperty in order to smooth the angle of my aircraft. However, if my points are say
Pos A Time 1
Pos B Time 2
Pos C Time 3
Pos C Time 4
Pos C Time 5
Pos D Time 6

Then, the airplane goes all haywire at point C, not understanding that it should be waiting at that point. In other words, I need to find a way to smooth out the path between a set of points (interpolation) but if the starting point and ending point are the same, I need to have the interpolated point be the same. It would also be nice if the plane doesn't change orientation from the last known velocity-based orientation, but that is less important.

Below is an example code for the plane going haywire at the fixed point. In this example, the fixed point is after the half-circle and before the straight line back. Any advice or workaround would be greatly appreciated!

var viewer = new Cesium.Viewer('cesiumContainer', {
    terrainProviderViewModels : , //Disable terrain changing
    infoBox : false, //Disable InfoBox widget
    selectionIndicator : false //Disable selection indicator
});

//Enable lighting based on sun/moon positions
viewer.scene.globe.enableLighting = true;

//Use STK World Terrain
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
    url : ‘https://assets.agi.com/stk-terrain/world’,
    requestWaterMask : true,
    requestVertexNormals : true
});

//Enable depth testing so things behind the terrain disappear.
viewer.scene.globe.depthTestAgainstTerrain = true;

//Set the random number seed for consistent results.
Cesium.Math.setRandomNumberSeed(3);

//Set bounds of our simulation time
var start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));
var stop = Cesium.JulianDate.addSeconds(start, 360, new Cesium.JulianDate());

//Make sure viewer is at the desired time.
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the end
viewer.clock.multiplier = 10;

//Set timeline to simulation bounds
viewer.timeline.zoomTo(start, stop);

//Generate a random circular pattern with varying heights.
function computeCirclularFlight(lon, lat, radius) {
    var property = new Cesium.SampledPositionProperty();
    var rnd=Cesium.Math.nextRandomNumber() ;
    for (var i = 0; i <= 360; i += 45) {
        var x = i;
        if (i>180 && i<360){
            x=180;
        }
        var radians = Cesium.Math.toRadians(x);
        var time = Cesium.JulianDate.addSeconds(start, i, new Cesium.JulianDate());
        var position = Cesium.Cartesian3.fromDegrees(lon + (radius * 1.5 * Math.cos(radians)), lat + (radius * Math.sin(radians)), rnd * 500 + 1750);
        property.addSample(time, position);
console.log(position.x);
        console.log(position.y);
        console.log(position.z);
        //Also create a point for each sample we generate.
        
        viewer.entities.add({
            position : position,
            point : {
                pixelSize : 8,
                color : Cesium.Color.TRANSPARENT,
                outlineColor : Cesium.Color.YELLOW,
                outlineWidth : 3
            }
        });
    }
    return property;
}

//Compute the entity position property.
var position = computeCirclularFlight(-112.110693, 36.0994841, 0.03);

//Actually create the entity
var entity = viewer.entities.add({

    //Set the entity availability to the same interval as the simulation time.
    availability : new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({
        start : start,
        stop : stop
    })]),

    //Use our computed positions
    position : position,

    //Automatically compute orientation based on position movement.
    orientation : new Cesium.VelocityOrientationProperty(position),

    //Load the Cesium plane model to represent the entity
    model : {
        uri : '../../SampleData/models/CesiumAir/Cesium_Air.gltf',
        minimumPixelSize : 64
    },

    //Show the path as a pink line sampled in 1 second increments.
    path : {
        resolution : 1,
        material : new Cesium.PolylineGlowMaterialProperty({
            glowPower : 0.1,
            color : Cesium.Color.YELLOW
        }),
        width : 10
    }
});

//Add button to view the path from the top down
Sandcastle.addDefaultToolbarButton('View Top Down', function() {
    viewer.trackedEntity = undefined;
    viewer.zoomTo(viewer.entities, new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90)));
});

//Add button to view the path from the side
Sandcastle.addToolbarButton('View Side', function() {
    viewer.trackedEntity = undefined;
    viewer.zoomTo(viewer.entities, new Cesium.HeadingPitchRange(Cesium.Math.toRadians(-90), Cesium.Math.toRadians(-15), 7500));
});

//Add button to track the entity as it moves
Sandcastle.addToolbarButton('View Aircraft', function() {
    viewer.trackedEntity = entity;
});

//Add a combo box for selecting each interpolation mode.
Sandcastle.addToolbarMenu([{
    text : 'Interpolation: Linear Approximation',
    onselect : function() {
        entity.position.setInterpolationOptions({
            interpolationDegree : 2,
            interpolationAlgorithm : Cesium.LinearApproximation
        });
    }
}, {
    text : 'Interpolation: Lagrange Polynomial Approximation',
    onselect : function() {
        entity.position.setInterpolationOptions({
            interpolationDegree :2,
            interpolationAlgorithm : Cesium.LagrangePolynomialApproximation
        });
    }
}, {
    text : 'Interpolation: Hermite Polynomial Approximation',
    onselect : function() {
        entity.position.setInterpolationOptions({
            interpolationDegree : 2,
            interpolationAlgorithm : Cesium.HermitePolynomialApproximation
        });
    }
}], 'interpolationMenu');

Adding image. Notice that the plane made a little jot in the bottom left while waiting to go back to the origin point.

image.png

image.png

image.png

After more research, this seems related to this issue:
https://groups.google.com/forum/embed/?place=forum/cesium-dev&showsearch=true&showpopout=true&hideforumtitle=true&fragments=true&parenturl=https%3A%2F%2Fcesiumjs.org%2Fforum.html#!searchin/cesium-dev/interpolation/cesium-dev/Ht_IPT6UMYA/ZBIwolsW7bcJ

However, I can not use the same recommendation (intervals) because I am not using CZML. Instead, I am adding samples to the sampledPositionProperty.

Is it possible to work around this, or add intervals, using the sampledPositionProperty directly?

Thanks,
Joy

image.png

image.png

image.png

Intervals in CZML end up creating CompositePositionProperty objects. See the example code for CompositeProperty and CompositePositionProperty:

http://cesiumjs.org/Cesium/Build/Documentation/CompositeProperty.html

http://cesiumjs.org/Cesium/Build/Documentation/CompositePositionProperty.html

image.png

image.png

image.png

So, just to be clear, a compositePositionProperty allows you to combine a sampledPositionProperty and a constantPositionProperty? This would mean that for this example, I could create a compositePositionProperty that had 3 intervals inside it. The first would be for the first half of the time which would be sampledPositionProperty. The second would be for the “holding still” part and would be a constantProperty, and the third would be for the “return home” and would be another sampledPositionProperty. Am I on the right track?

Thanks!

image.png

image.png

image.png

Yes, that would work. Alternatively you could use two intervals, [start, return] and [return, end] and configure the first sampled property with forwardExtrapolationType HOLD, so it holds position of the last sample after that time (between time of arrival and time of return).

image.png

image.png

image.png

Scott, thank you! I just tested my clarification and it works well in sandcastle. Just need to figure out how to get that working in my app!

Just for anyone else with this issue, I have included the final solution below:

var viewer = new Cesium.Viewer(‘cesiumContainer’, {

terrainProviderViewModels : , //Disable terrain changing

infoBox : false, //Disable InfoBox widget

selectionIndicator : false //Disable selection indicator

});

//Enable lighting based on sun/moon positions

viewer.scene.globe.enableLighting = true;

//Use STK World Terrain

viewer.terrainProvider = new Cesium.CesiumTerrainProvider({

url : ‘https://assets.agi.com/stk-terrain/world’,

requestWaterMask : true,

requestVertexNormals : true

});

//Enable depth testing so things behind the terrain disappear.

viewer.scene.globe.depthTestAgainstTerrain = true;

//Set the random number seed for consistent results.

Cesium.Math.setRandomNumberSeed(3);

//Set bounds of our simulation time

var start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));

var stop = Cesium.JulianDate.addSeconds(start, 360, new Cesium.JulianDate());

//Make sure viewer is at the desired time.

viewer.clock.startTime = start.clone();

viewer.clock.stopTime = stop.clone();

viewer.clock.currentTime = start.clone();

viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the end

viewer.clock.multiplier = 10;

//Set timeline to simulation bounds

viewer.timeline.zoomTo(start, stop);

//Generate a random circular pattern with varying heights.

function computeCirclularFlight(lon, lat, radius) {

var start1=start;

var start2;

var start3;

var end1;

var end2;

var end3;

//Create a composite property from two previously defined properties

//where the property is valid on August 1st, 2012 and uses a constant

//property for the first half of the day and a sampled property for the

//remaining half.

var sproperty1 = new Cesium.SampledPositionProperty(

);

var cproperty = new Cesium.ConstantPositionProperty(

);

var sproperty2 = new Cesium.SampledPositionProperty(

);

sproperty1.setInterpolationOptions({

interpolationDegree :2,

interpolationAlgorithm : Cesium.LagrangePolynomialApproximation

});

sproperty2.setInterpolationOptions({

interpolationDegree :2,

interpolationAlgorithm : Cesium.LagrangePolynomialApproximation

});

var intervalStart = start;

var rnd=Cesium.Math.nextRandomNumber() ;

var h = rnd * 500 + 1750;

for (var i = 0; i <= 360; i += 45) {

var n = i;

if (n>=180 && n<360){

n=135;

}

var radians = Cesium.Math.toRadians(n);

var time = Cesium.JulianDate.addSeconds(start, i, new Cesium.JulianDate());

var x = lon + (radius * 1.5 * Math.cos(radians));

var y = lat + (radius * Math.sin(radians));

var z = h;

var position = Cesium.Cartesian3.fromDegrees(x, y, z);

if (i===135){

end1=time;

start2=time;

}

if (i===315){

end2=time;

start3=time;

}

if (i===360){

end3=time;

}

if (i<=135){

sproperty1.addSample(time,position);

}

if (i===135){

cproperty.setValue(position);

}

if (i >=315){

sproperty2.addSample(time,position);

}

//Also create a point for each sample we generate.

viewer.entities.add({

position : position,

point : {

pixelSize : 8,

color : Cesium.Color.TRANSPARENT,

outlineColor : Cesium.Color.YELLOW,

outlineWidth : 3

}

});

}

var composite = new Cesium.CompositePositionProperty();

var timeInterval1 = new Cesium.TimeInterval({

start : start1,

stop : end1,

isStartIncluded : true,

isStopIncluded : true,

data : sproperty1

});

var timeInterval2 = new Cesium.TimeInterval({

start : start2,

stop : end2,

isStartIncluded : false,

isStopIncluded : false,

data : cproperty

});

var timeInterval3 = new Cesium.TimeInterval({

start : start3,

stop : end3,

isStartIncluded : true,

isStopIncluded : true,

data : sproperty2

});

composite.intervals.addInterval(timeInterval1);

composite.intervals.addInterval(timeInterval2);

composite.intervals.addInterval(timeInterval3);

return composite;

}

//Compute the entity position property.

var position = computeCirclularFlight(-112.110693, 36.0994841, 0.03);

//Actually create the entity

var entity = viewer.entities.add({

//Set the entity availability to the same interval as the simulation time.

availability : new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({

start : start,

stop : stop

})]),

//Use our computed positions

position : position,

//Automatically compute orientation based on position movement.

orientation : new Cesium.VelocityOrientationProperty(position),

//Load the Cesium plane model to represent the entity

model : {

uri : ‘…/…/SampleData/models/CesiumAir/Cesium_Air.gltf’,

minimumPixelSize : 64

},

//Show the path as a pink line sampled in 1 second increments.

path : {

resolution : 1,

material : new Cesium.PolylineGlowMaterialProperty({

glowPower : 0.1,

color : Cesium.Color.YELLOW

}),

width : 10

}

});

//Add button to view the path from the top down

Sandcastle.addDefaultToolbarButton(‘View Top Down’, function() {

viewer.trackedEntity = undefined;

viewer.zoomTo(viewer.entities, new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90)));

});

//Add button to view the path from the side

Sandcastle.addToolbarButton(‘View Side’, function() {

viewer.trackedEntity = undefined;

viewer.zoomTo(viewer.entities, new Cesium.HeadingPitchRange(Cesium.Math.toRadians(-90), Cesium.Math.toRadians(-15), 7500));

});

//Add button to track the entity as it moves

Sandcastle.addToolbarButton(‘View Aircraft’, function() {

viewer.trackedEntity = entity;

});

image.png

image.png

image.png

One last thing…

I did a little more testing and found that I could use just two intervals. The first covered the whole time (start1-end3) and the second interval covered the stopped time. I added the stopped interval second so that it would take precedence over the first interval.

Then, I changed the code that added to the properties to the below code:

sproperty3.addSample(time,position);

to

sproperty1.addSample(time,position);

and removed sproperty3 and timeInterval3;

This way, the samples before and after the stop were added to the single sampledPositionProperty (which was set to the interval that covered the whole span).

The whole code is below.

My question is first whether I can count on the system to use the last interval added if two are overlapping. Also, I want to know if using overlapped intervals is an acceptable way to do this. It will dramatically reduce the number of intervals if there are lots of starts and stops (about by half the number of intervals + 1 for the full interval). However, it depends on whether the system is designed to dea welll with overlaps. I assume that this is ok, but want to know if the performance of dealing with overlap is problematic enough that I should avoid it when necessary.

Modified code below:

var viewer = new Cesium.Viewer(‘cesiumContainer’, {

terrainProviderViewModels : , //Disable terrain changing

infoBox : false, //Disable InfoBox widget

selectionIndicator : false //Disable selection indicator

});

//Enable lighting based on sun/moon positions

viewer.scene.globe.enableLighting = true;

//Use STK World Terrain

viewer.terrainProvider = new Cesium.CesiumTerrainProvider({

url : ‘https://assets.agi.com/stk-terrain/world’,

requestWaterMask : true,

requestVertexNormals : true

});

//Enable depth testing so things behind the terrain disappear.

viewer.scene.globe.depthTestAgainstTerrain = true;

//Set the random number seed for consistent results.

Cesium.Math.setRandomNumberSeed(3);

//Set bounds of our simulation time

var start = Cesium.JulianDate.fromDate(new Date(2015, 2, 25, 16));

var stop = Cesium.JulianDate.addSeconds(start, 360, new Cesium.JulianDate());

//Make sure viewer is at the desired time.

viewer.clock.startTime = start.clone();

viewer.clock.stopTime = stop.clone();

viewer.clock.currentTime = start.clone();

viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP; //Loop at the end

viewer.clock.multiplier = 10;

//Set timeline to simulation bounds

viewer.timeline.zoomTo(start, stop);

//Generate a random circular pattern with varying heights.

function computeCirclularFlight(lon, lat, radius) {

var start1=start;

var start2;

// var start3;

// var end1;

var end2;

var end3;

//Create a composite property from two previously defined properties

//where the property is valid on August 1st, 2012 and uses a constant

//property for the first half of the day and a sampled property for the

//remaining half.

var sproperty1 = new Cesium.SampledPositionProperty(

);

var cproperty = new Cesium.ConstantPositionProperty(

);

// var sproperty2 = new Cesium.SampledPositionProperty(

// );

sproperty1.setInterpolationOptions({

interpolationDegree :2,

interpolationAlgorithm : Cesium.LagrangePolynomialApproximation

});

// sproperty2.setInterpolationOptions({

// interpolationDegree :2,

// interpolationAlgorithm : Cesium.LagrangePolynomialApproximation

// });

var intervalStart = start;

var rnd=Cesium.Math.nextRandomNumber() ;

var h = rnd * 500 + 1750;

for (var i = 0; i <= 360; i += 45) {

var n = i;

if (n>=180 && n<360){

n=135;

}

var radians = Cesium.Math.toRadians(n);

var time = Cesium.JulianDate.addSeconds(start, i, new Cesium.JulianDate());

var x = lon + (radius * 1.5 * Math.cos(radians));

var y = lat + (radius * Math.sin(radians));

var z = h;

var position = Cesium.Cartesian3.fromDegrees(x, y, z);

if (i===135){

// end1=time;

start2=time;

}

if (i===315){

end2=time;

// start3=time;

}

if (i===360){

end3=time;

}

if (i<=135){

sproperty1.addSample(time,position);

}

if (i===135){

cproperty.setValue(position);

}

if (i >=315){

sproperty1.addSample(time,position);

//sproperty2.addSample(time,position);

}

//Also create a point for each sample we generate.

viewer.entities.add({

position : position,

point : {

pixelSize : 8,

color : Cesium.Color.TRANSPARENT,

outlineColor : Cesium.Color.YELLOW,

outlineWidth : 3

}

});

}

var composite = new Cesium.CompositePositionProperty();

var timeInterval1 = new Cesium.TimeInterval({

start : start1,

//stop : end1,

stop : end3,

isStartIncluded : true,

isStopIncluded : true,

data : sproperty1

});

var timeInterval2 = new Cesium.TimeInterval({

start : start2,

stop : end2,

isStartIncluded : false,

isStopIncluded : false,

data : cproperty

});

/*

var timeInterval3 = new Cesium.TimeInterval({

start : start3,

stop : end3,

isStartIncluded : true,

isStopIncluded : true,

data : sproperty2

});

*/

composite.intervals.addInterval(timeInterval1);

composite.intervals.addInterval(timeInterval2);

//composite.intervals.addInterval(timeInterval3);

return composite;

}

//Compute the entity position property.

var position = computeCirclularFlight(-112.110693, 36.0994841, 0.03);

//Actually create the entity

var entity = viewer.entities.add({

//Set the entity availability to the same interval as the simulation time.

availability : new Cesium.TimeIntervalCollection([new Cesium.TimeInterval({

start : start,

stop : stop

})]),

//Use our computed positions

position : position,

//Automatically compute orientation based on position movement.

orientation : new Cesium.VelocityOrientationProperty(position),

//Load the Cesium plane model to represent the entity

model : {

uri : ‘…/…/SampleData/models/CesiumAir/Cesium_Air.gltf’,

minimumPixelSize : 64

},

//Show the path as a pink line sampled in 1 second increments.

path : {

resolution : 1,

material : new Cesium.PolylineGlowMaterialProperty({

glowPower : 0.1,

color : Cesium.Color.YELLOW

}),

width : 10

}

});

//Add button to view the path from the top down

Sandcastle.addDefaultToolbarButton(‘View Top Down’, function() {

viewer.trackedEntity = undefined;

viewer.zoomTo(viewer.entities, new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-90)));

});

//Add button to view the path from the side

Sandcastle.addToolbarButton(‘View Side’, function() {

viewer.trackedEntity = undefined;

viewer.zoomTo(viewer.entities, new Cesium.HeadingPitchRange(Cesium.Math.toRadians(-90), Cesium.Math.toRadians(-15), 7500));

});

//Add button to track the entity as it moves

Sandcastle.addToolbarButton(‘View Aircraft’, function() {

viewer.trackedEntity = entity;

});

image.png

image.png

image.png

Great, thank you! That makes perfect sense.

For my particular code, it will be easiest to just add in the “stopped” intervals (and convert the sampledPositionProperty to a compositePositionedProperty with the sampledPositionProperty inside).

However, I will keep this in mind in case this alternative approach will help with something else in the future.

image.png

image.png

image.png