Skip to content

Commit

Permalink
enhancement/1624 - Add speed capability to crossing restrictions
Browse files Browse the repository at this point in the history
  • Loading branch information
jaysella authored and erikquinn committed Jul 12, 2020
1 parent 25f8e1e commit 455d924
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 38 deletions.
6 changes: 3 additions & 3 deletions documentation/aircraft-commands.md
Expand Up @@ -350,11 +350,11 @@ the airplane, as long as they comply with the restrictions given.

## Cross

_Aliases -_ `cross`, `cr`
_Aliases -_ `cross`, `cr`, `x`

_Information -_ This command has the aircraft cross a specified point along their route at a specified altitude. The altitude must be entered in hundreds of feet (eg `130` for 13,000 feet).
_Information -_ This command has the aircraft cross a specified point along their route at a specified altitude and/or speed. The altitude must be entered in hundreds of feet (eg `130` for 13,000 feet). The speed should be entered in knots (eg `210` for 210 knots).

_Syntax -_ `AAL123 cr aubrn 130`
_Syntax -_ `AAL123 x aubrn a[alt] s[spd]`

## Aircraft Query Commands

Expand Down
3 changes: 2 additions & 1 deletion src/assets/scripts/client/aircraft/AircraftCommander.js
Expand Up @@ -257,8 +257,9 @@ export default class AircraftCommander {
runCross(aircraft, data) {
const fix = data[0].toUpperCase();
const altitude = data[1];
const speed = data[2];

return aircraft.pilot.crossFix(aircraft, fix, altitude);
return aircraft.pilot.crossFix(aircraft, fix, altitude, speed);
}

/**
Expand Down
29 changes: 29 additions & 0 deletions src/assets/scripts/client/aircraft/AircraftModel.js
Expand Up @@ -2850,4 +2850,33 @@ export default class AircraftModel {

return [true];
}

/**
* Ensure that the provided speed is valid
*
* @for AircraftModel
* @method validateNextSpeed
* @param nextSpeed {number} speed the aircraft should maintain
* @return {array} [success of operation, readback]
*/
validateNextSpeed(nextSpeed) {
if (nextSpeed === INVALID_NUMBER) {
return [false, 'unable, no speed assigned'];
}

if (typeof nextSpeed !== 'number') {
return [false, `unable to maintain a speed of ${nextSpeed}`];
}

if (!this.model.isAbleToMaintainSpeed(nextSpeed)) {
const readback = {
log: `unable to maintain ${nextSpeed} due to performance`,
say: `unable to maintain ${radio_spellOut(nextSpeed)} knots due to performance`
};

return [false, readback];
}

return [true];
}
}
Expand Up @@ -500,6 +500,54 @@ export default class WaypointModel {
this._holdParameters.timer = DEFAULT_HOLD_PARAMETERS.timer;
}

/**
* Set the #speedMinimum and #speedMaximum to the specified speed
*
* @for WaypointModel
* @method setSpeed
* @param speec {number} in knots
*/
setSpeed(speed) {
this.setSpeedMinimum(speed);
this.setSpeedMaximum(speed);
}

/**
* Set the #_speedMaximum to the specified speed
*
* @for WaypointModel
* @method setSpeedMaximum
* @param speedMaximum {number} in knots
*/
setSpeedMaximum(speedMaximum) {
if (!_isNumber(speedMaximum)) {
console.warn(`Expected number to set as max speed of waypoint '${this.name}', ` +
`but received '${speedMaximum}'`);

return;
}

this._speedMaximum = speedMaximum;
}

/**
* Set the #_speedMinimum to the specified speed
*
* @for WaypointModel
* @method setSpeedMinimum
* @param speedMinimum {number} in knots
*/
setSpeedMinimum(speedMinimum) {
if (!_isNumber(speedMinimum)) {
console.warn(`Expected number to set as minimum speed of waypoint '${this.name}', ` +
`but received '${speedMinimum}'`);

return;
}

this._speedMinimum = speedMinimum;
}

/**
* Set the #altitudeMinimum and #altitudeMaximum to the specified altitude
*
Expand Down
68 changes: 61 additions & 7 deletions src/assets/scripts/client/aircraft/Pilot/Pilot.js
Expand Up @@ -564,16 +564,21 @@ export default class Pilot {
}

/**
* Cross a fix at a certain altitude
* Cross a fix at a certain altitude and/or speed
*
* @for Pilot
* @method crossFix
* @param aircraftModel {AircraftModel}
* @param fixName {string} name of the fix
* @param altitude {number} the altitude
* @param speed {number} the speed
* @return {array} success of operation, readback]
*/
crossFix(aircraftModel, fixName, altitude) {
crossFix(aircraftModel, fixName, altitude, speed) {
if (!altitude && !speed) {
return [false, 'say again? In crossing restrictions, prefix altitudes with A and speeds with S!'];
}

if (!NavigationLibrary.hasFixName(fixName)) {
return [false, `unable to find '${fixName}'`];
}
Expand All @@ -583,23 +588,72 @@ export default class Pilot {
}

const airportModel = this._fms.arrivalAirportModel || this._fms.departureAirportModel;
const waypoint = this._fms.findWaypoint(fixName);

// altitude-only crossing restriction
if (!speed) {
const altitudeCheck = aircraftModel.validateNextAltitude(altitude, airportModel);

if (!altitudeCheck[0]) {
return altitudeCheck;
}

altitude = airportModel.clampWithinAssignableAltitudes(altitude);

waypoint.setAltitude(altitude);
this._mcp.setAltitudeFieldValue(altitude);
this._mcp.setAltitudeVnav();

const readback = {
log: `cross ${fixName.toUpperCase()} at ${altitude}`,
say: `cross ${fixName.toLowerCase()} at ${radio_altitude(altitude)}`
};

return [true, readback];
}

// speed-only crossing restriction
if (!altitude) {
const speedCheck = aircraftModel.validateNextSpeed(speed);

if (!speedCheck[0]) {
return speedCheck;
}

waypoint.setSpeed(speed);
this._mcp.setSpeedFieldValue(speed);
this._mcp.setSpeedVnav();

const readback = {
log: `cross ${fixName.toUpperCase()} at ${speed}kt`,
say: `cross ${fixName.toLowerCase()} at ${radio_spellOut(speed)} knots`
};

return [true, readback];
}

// altitude AND speed crossing restriction
const altitudeCheck = aircraftModel.validateNextAltitude(altitude, airportModel);
const speedCheck = aircraftModel.validateNextSpeed(speed);

if (!altitudeCheck[0]) {
return altitudeCheck;
}

altitude = airportModel.clampWithinAssignableAltitudes(altitude);

const waypoint = this._fms.findWaypoint(fixName);
if (!speedCheck[0]) {
return speedCheck;
}

waypoint.setAltitude(altitude);
waypoint.setSpeed(speed);
this._mcp.setAltitudeFieldValue(altitude);
this._mcp.setSpeedFieldValue(speed);
this._mcp.setAltitudeVnav();
this._mcp.setSpeedVnav();

const readback = {
log: `cross ${fixName.toUpperCase()} at ${altitude}`,
say: `cross ${fixName.toLowerCase()} at ${radio_altitude(altitude)}`
log: `cross ${fixName.toUpperCase()} at ${altitude} and ${speed}kt`,
say: `cross ${fixName.toLowerCase()} at ${radio_altitude(altitude)} and ${radio_spellOut(speed)} knots`
};

return [true, readback];
Expand Down
Expand Up @@ -60,7 +60,7 @@ export const AIRCRAFT_COMMAND_MAP = {
isSystemCommand: false
},
cross: {
aliases: ['cross', 'cr'],
aliases: ['cross', 'cr', 'x'],
functionName: 'runCross',
isSystemCommand: false
},
Expand Down
Expand Up @@ -32,8 +32,10 @@ export const ERROR_MESSAGE = {
ONE_OR_TWO_ARG_LENGTH: `${INVALID_ARG_LENGTH}. Expected one or two arguments`,
ONE_TO_THREE_ARG_LENGTH: `${INVALID_ARG_LENGTH}. Expected one, two, or three arguments`,
ONE_OR_THREE_ARG_LENGTH: `${INVALID_ARG_LENGTH}. Expected one or three arguments`,
ALTITUDE_MUST_BE_NUMBER: `${INVALID_ARG}. Altitude must be a number`,
TWO_OR_THREE_ARG_LENGTH: `${INVALID_ARG_LENGTH}. Expected two or three arguments`,
ALTITUDE_EXPEDITE_ARG: `${INVALID_ARG}. Altitude accepts only "expedite" or "x" as a second argument`,
ALTITUDE_MUST_BE_NUMBER: `${INVALID_ARG}. Altitude must be a number`,
SPEED_MUST_BE_NUMBER: `${INVALID_ARG}. Speed must be a number`,
HEADING_MUST_BE_NUMBER: `${INVALID_ARG}. Heading must be a number`,
HEADING_MUST_BE_VALID_COURSE: `${INVALID_ARG}. Heading must be between 001 and 360`,
INCREMENTAL_HEADING_MUST_BE_POSITIVE: `${INVALID_ARG}. Incremental heading must be positive`,
Expand Down
Expand Up @@ -230,8 +230,18 @@ export const timewarpParser = (args = []) => {
*/
export const crossingParser = (args = []) => {
const fix = args[0];
const altitude = convertToThousands(args[1]);
// TODO: Add logic for speeds at fix (eg "250K" means to cross at 250kt, while "250" means cross at FL250)

return [fix, altitude];
let altitude;
let speed;

// Set i to 1 to skip fixName
for (let i = 1; i < args.length; i++) {
if (args[i][0].toLowerCase() === 'a') {
altitude = convertToThousands(args[i].toString().substr(1));
} else if (args[i][0].toLowerCase() === 's') {
speed = convertStringToNumber(args[i].toString().substr(1));
}
}

return [fix, altitude, speed];
};
Expand Up @@ -87,6 +87,19 @@ export const oneOrThreeArgumentsValidator = (args = []) => {
}
};

/**
* Checks that `args` has exactly two or three values
*
* @function twoOrThreeArgumentsValidator
* @param args {array}
* @return {string|undefined}
*/
export const twoOrThreeArgumentsValidator = (args = []) => {
if (args.length !== 2 && args.length !== 3) {
return ERROR_MESSAGE.TWO_OR_THREE_ARG_LENGTH;
}
};

/**
* Checks that args is the required length and the data is of the correct type
*
Expand Down Expand Up @@ -311,28 +324,59 @@ export const squawkValidator = (args = []) => {
*
* ```
* Allowed argument shapes:
* - ['dumba', '120']
* - ['dumba', 'a120', 's210']
* ```
*
* @function crossingValidator
* @param args {array}
* @return {array<string>}
*/
export const crossingValidator = (args = []) => {
if (args.length !== 2) {
return ERROR_MESSAGE.TWO_ARG_LENGTH;
if (args.length !== 2 && args.length !== 3) {
return ERROR_MESSAGE.TWO_OR_THREE_ARG_LENGTH;
}

const [fixName] = args;
let altitude = args[1];

let altitude;
let speed;

// Set i to 1 to skip fixName
for (let i = 1; i < args.length; i++) {
if (typeof args[i][0] === 'string') {
if (args[i][0].toLowerCase() === 'a') {
altitude = args[i].toString().substr(1);
} else if (args[i][0].toLowerCase() === 's') {
speed = args[i].toString().substr(1);
}
}
}

if (!_isString(fixName)) {
return ERROR_MESSAGE.MUST_BE_STRING;
}

altitude = convertStringToNumber(altitude);
if (altitude) {
altitude = convertStringToNumber(altitude);

if (_isNaN(altitude)) {
return ERROR_MESSAGE.ALTITUDE_MUST_BE_NUMBER;
if (_isNaN(altitude)) {
return ERROR_MESSAGE.ALTITUDE_MUST_BE_NUMBER;
}
}

if (speed) {
speed = convertStringToNumber(speed);

if (_isNaN(speed)) {
return ERROR_MESSAGE.SPEED_MUST_BE_NUMBER;
}
}

if (args.length === 3) {
if (altitude == null) {
return ERROR_MESSAGE.ALTITUDE_MUST_BE_NUMBER;
} else if (speed == null) {
return ERROR_MESSAGE.SPEED_MUST_BE_NUMBER;
}
}
};
19 changes: 17 additions & 2 deletions test/parsers/aircraftCommandParser/argumentParser.spec.js
Expand Up @@ -205,9 +205,24 @@ ava('.timewarpParser() returns an array with 50 as a value when provided as an a
});


ava('.crossingParser() returns an array with the correct values', (t) => {
const result = crossingParser(['LEMDY', '50']);
ava('.crossingParser() returns an array with the correct values when provided all args', (t) => {
const result = crossingParser(['LEMDY', 'a50', 's210']);

t.true(result[0] === 'LEMDY');
t.true(result[1] === 5000);
t.true(result[2] === 210);
});

ava('.crossingParser() returns an array with the correct values when provided altitude as an arg', (t) => {
const result = crossingParser(['LEMDY', 'a50']);

t.true(result[0] === 'LEMDY');
t.true(result[1] === 5000);
});

ava('.crossingParser() returns an array with the correct values when provided speed as an arg', (t) => {
const result = crossingParser(['LEMDY', 's210']);

t.true(result[0] === 'LEMDY');
t.true(result[2] === 210);
});

0 comments on commit 455d924

Please sign in to comment.