I've been writing a function:
float turnToRequestedHeading(float initialHeading, float requiredHeading, float turnRate)
I keep thinking there must be a clever way to do it, but it escapes me.
All values are in Radians, Headings between -\$\pi\$ and +\$\pi\$, and turnRate between -0.5 and +0.5.
If the requiredHeading is less than the turnRate away from the initialHeading then it should return requiredHeading
Otherwise it should return initialHeading + or - turnRate, whichever gets closer to the requiredHeading.
The following code is what I have so far, which I'm quite happy with, but is there anything I've missed, or is there an easier way to do this?
// return the new heading based on the required heading and turn rate
private float turnToRequestedHeading(float initialHeading, float requiredHeading, float turnRate) {
//DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Started");
float resultantHeading;
int direction = 1; // clockwise, set anti-clockwise (-1) later if required
if ((Math.signum(initialHeading) == Math.signum(requiredHeading)) || (Math.signum(initialHeading) == 0) || (Math.signum(requiredHeading) == 0)) {
// both headings are on the same side of 0 so turn will not pass through the +/- Pi discontinuity
if (Math.max(Math.abs(requiredHeading) - Math.abs(initialHeading), Math.abs(initialHeading) - Math.abs(requiredHeading)) < turnRate) {
// angle to be updated is less than turn rate
resultantHeading= requiredHeading;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path1");
} else {
// angle to be updated is greater than turn rate
if (initialHeading < requiredHeading) {
// turn clockwise
resultantHeading = initialHeading + turnRate;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path2");
} else {
// turn anti-clockwise
resultantHeading = initialHeading - turnRate;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path3");
}
}
} else {
// headings are on different sides of 0 so turn may pass through the +/- Pi discontinuity
if (Math.abs(initialHeading) + Math.abs(requiredHeading) < turnRate) {
// angle to be updated is less than turn rate (around 0)
resultantHeading= requiredHeading;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path4");
} else if ((180 - Math.abs(initialHeading)) + (180 - Math.abs(requiredHeading)) < turnRate) {
// angle to be updated is less than turn rate (around +/- Pi)
resultantHeading= requiredHeading;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path5");
} else {
// angle to be updated is greater than turn rate so calculate direction (previously assumed to be 1)
if (initialHeading < 0) {
if (requiredHeading > PIf + initialHeading) direction = -1;
} else {
if (requiredHeading > -PIf + initialHeading) direction = -1;
}
if ((direction == 1) && (initialHeading > PIf - turnRate)) {
// angle includes the +/- Pi discontinuity, clockwise
resultantHeading = -TWO_PIf + turnRate + initialHeading;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path6 snap="+(resultantHeading > requiredHeading));
if (resultantHeading > requiredHeading) resultantHeading = requiredHeading;
} else if ((direction == -1) && (initialHeading < -PIf + turnRate)) {
// angle includes the +/- Pi discontinuity, anti-clockwise
resultantHeading = TWO_PIf - turnRate + initialHeading;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path7 snap="+(resultantHeading < requiredHeading));
if (resultantHeading < requiredHeading) resultantHeading = requiredHeading;
} else {
// angle does not includes the +/- Pi discontinuity
resultantHeading = initialHeading + direction * turnRate;
/*DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading(initialHeading="+initialHeading+", requiredHeading="+requiredHeading+", turnRate="+turnRate+"): Path8 direction="+direction);
}
}
}
// ensure -PI <= result <= PI
if (resultantHeading < -PIf) resultantHeading = resultantHeading + TWO_PIf;
if (resultantHeading >= PIf) resultantHeading = resultantHeading - TWO_PIf;
//DEBUG*/Log.d(this.getClass().getName(), "turnToRequestedHeading: Returning "+resultantHeading);
return resultantHeading;
}
I have written some testing code to check it out as there were quite a few different 'special cases'
private void turnToRequestedHeadingTest(float initialHeading, float requiredHeading, float turnRate, float expectedResult) {
if (Math.round(turnToRequestedHeading(initialHeading*PIf/180, requiredHeading*PIf/180, turnRate*PIf/180)*180/PIf) != expectedResult) {
/*DEBUG*/Log.i(this.getClass().getName(), "test(initial="+initialHeading+", required="+requiredHeading+", rate="+turnRate+") Expected "+expectedResult+", Returns "+(Math.round(turnToRequestedHeading(initialHeading*PIf/180, requiredHeading*PIf/180, turnRate*PIf/180)*180/PIf)));
}
}
/*DEBUG*/Log.i(this.getClass().getName(), "turnToRequestedHeading tests:");
turnToRequestedHeadingTest( 0, 0, 0, 0);
turnToRequestedHeadingTest( 0, 0, 25, 0);
turnToRequestedHeadingTest( 10, 15, 25, 15);
turnToRequestedHeadingTest( 20, 55, 25, 45);
turnToRequestedHeadingTest( 85, 95, 25, 95);
turnToRequestedHeadingTest( 150,-170, 25, 175);
turnToRequestedHeadingTest( 170, 177, 25, 177);
turnToRequestedHeadingTest( 170,-175, 25,-175);
turnToRequestedHeadingTest( 175,-100, 25,-160);
turnToRequestedHeadingTest( 175, 0, 25, 150);
turnToRequestedHeadingTest( 180, 0, 25, 155);
turnToRequestedHeadingTest(-170,-100, 25,-145);
turnToRequestedHeadingTest(-100, -80, 25, -80);
turnToRequestedHeadingTest( -30, -15, 25, -15);
turnToRequestedHeadingTest( -30, 15, 25, -5);
turnToRequestedHeadingTest( -20, -5, 25, -5);
turnToRequestedHeadingTest( -20, 5, 25, 5);
turnToRequestedHeadingTest( -20, 15, 25, 5);
turnToRequestedHeadingTest( 10, 180, 25, 35);
turnToRequestedHeadingTest( 10,-160, 25, -15);
turnToRequestedHeadingTest( 170, 0, 25, 145);
turnToRequestedHeadingTest( 170, -15, 25,-165);
turnToRequestedHeadingTest(-170, 5, 25,-145);
turnToRequestedHeadingTest( -10, 160, 25, 15);
turnToRequestedHeadingTest( -10,-150, 25, -35);
turnToRequestedHeadingTest( 10,-170, 25, -15);
turnToRequestedHeadingTest( 0, 180, 25, 25);
turnToRequestedHeadingTest( -10, -15, 25, -15);
turnToRequestedHeadingTest( -20, -55, 25, -45);
turnToRequestedHeadingTest( -85, -95, 25, -95);
turnToRequestedHeadingTest(-150, 170, 25,-175);
turnToRequestedHeadingTest(-170,-177, 25,-177);
turnToRequestedHeadingTest(-170, 175, 25, 175);
turnToRequestedHeadingTest(-175, 100, 25, 160);
turnToRequestedHeadingTest(-175, 0, 25,-150);
turnToRequestedHeadingTest( 170, 100, 25, 145);
turnToRequestedHeadingTest( 100, 80, 25, 80);
turnToRequestedHeadingTest( 30, 15, 25, 15);
turnToRequestedHeadingTest( 30, -15, 25, 5);
turnToRequestedHeadingTest( 20, 5, 25, 5);
turnToRequestedHeadingTest( 20, -5, 25, -5);
turnToRequestedHeadingTest( 20, -15, 25, -5);
turnToRequestedHeadingTest( -10,-180, 25, -35);
turnToRequestedHeadingTest( -10, 160, 25, 15);
turnToRequestedHeadingTest(-170, 0, 25,-145);
turnToRequestedHeadingTest(-170, 15, 25, 165);
turnToRequestedHeadingTest( 170, -5, 25, 145);
turnToRequestedHeadingTest( 10,-160, 25, -15);
turnToRequestedHeadingTest( 10, 150, 25, 35);
turnToRequestedHeadingTest( -10, 170, 25, 15);
// More tests
turnToRequestedHeadingTest( 0, 15, 25, 15);
turnToRequestedHeadingTest( 0, 60, 25, 25);
turnToRequestedHeadingTest( 0, -15, 25, -15);
turnToRequestedHeadingTest( 0, -60, 25, -25);
turnToRequestedHeadingTest( 180, 165, 25, 165);
turnToRequestedHeadingTest( 180, 100, 25, 155);
turnToRequestedHeadingTest( 180,-165, 25,-165);
turnToRequestedHeadingTest( 180,-100, 25,-155);
turnToRequestedHeadingTest(-180, 165, 25, 165);
turnToRequestedHeadingTest(-180, 100, 25, 155);
turnToRequestedHeadingTest(-180,-165, 25,-165);
turnToRequestedHeadingTest(-180,-100, 25,-155);
turnToRequestedHeadingTest( 25, 0, 25, 0);
turnToRequestedHeadingTest( 25, -25, 25, 0);
turnToRequestedHeadingTest( -25, 0, 25, 0);
turnToRequestedHeadingTest( -25, 25, 25, 0);
turnToRequestedHeadingTest( 155, 180, 25, 180);
turnToRequestedHeadingTest( 155,-155, 25, 180);
turnToRequestedHeadingTest(-155, 180, 25,-180);
turnToRequestedHeadingTest(-155, 155, 25,-180);
turnToRequestedHeadingTest( 155,-180, 25,-180);
turnToRequestedHeadingTest(-155,-180, 25,-180);