From 455d924a31ab979623cbd94adc26dbf90ba81cf3 Mon Sep 17 00:00:00 2001 From: Jay S Date: Sat, 8 Feb 2020 05:23:23 -0600 Subject: [PATCH] enhancement/1624 - Add speed capability to crossing restrictions --- documentation/aircraft-commands.md | 6 +- .../client/aircraft/AircraftCommander.js | 3 +- .../scripts/client/aircraft/AircraftModel.js | 29 ++++++++ .../FlightManagementSystem/WaypointModel.js | 48 +++++++++++++ .../scripts/client/aircraft/Pilot/Pilot.js | 68 +++++++++++++++++-- .../aircraftCommandMap.js | 2 +- .../aircraftCommandParserMessages.js | 4 +- .../aircraftCommandParser/argumentParsers.js | 16 ++++- .../argumentValidators.js | 58 ++++++++++++++-- .../argumentParser.spec.js | 19 +++++- .../argumentValidator.spec.js | 40 +++++++---- 11 files changed, 255 insertions(+), 38 deletions(-) diff --git a/documentation/aircraft-commands.md b/documentation/aircraft-commands.md index aa46615ec7..c7bb204da2 100644 --- a/documentation/aircraft-commands.md +++ b/documentation/aircraft-commands.md @@ -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 diff --git a/src/assets/scripts/client/aircraft/AircraftCommander.js b/src/assets/scripts/client/aircraft/AircraftCommander.js index f1e22fbd0b..a2138eedb4 100644 --- a/src/assets/scripts/client/aircraft/AircraftCommander.js +++ b/src/assets/scripts/client/aircraft/AircraftCommander.js @@ -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); } /** diff --git a/src/assets/scripts/client/aircraft/AircraftModel.js b/src/assets/scripts/client/aircraft/AircraftModel.js index 9ab7286b1b..8edd9dfdcd 100644 --- a/src/assets/scripts/client/aircraft/AircraftModel.js +++ b/src/assets/scripts/client/aircraft/AircraftModel.js @@ -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]; + } } diff --git a/src/assets/scripts/client/aircraft/FlightManagementSystem/WaypointModel.js b/src/assets/scripts/client/aircraft/FlightManagementSystem/WaypointModel.js index 34789310e7..73673407c9 100644 --- a/src/assets/scripts/client/aircraft/FlightManagementSystem/WaypointModel.js +++ b/src/assets/scripts/client/aircraft/FlightManagementSystem/WaypointModel.js @@ -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 * diff --git a/src/assets/scripts/client/aircraft/Pilot/Pilot.js b/src/assets/scripts/client/aircraft/Pilot/Pilot.js index 4c7edde1e0..afeee1d92c 100644 --- a/src/assets/scripts/client/aircraft/Pilot/Pilot.js +++ b/src/assets/scripts/client/aircraft/Pilot/Pilot.js @@ -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}'`]; } @@ -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]; diff --git a/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandMap.js b/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandMap.js index 7975c628ff..b459853ade 100644 --- a/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandMap.js +++ b/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandMap.js @@ -60,7 +60,7 @@ export const AIRCRAFT_COMMAND_MAP = { isSystemCommand: false }, cross: { - aliases: ['cross', 'cr'], + aliases: ['cross', 'cr', 'x'], functionName: 'runCross', isSystemCommand: false }, diff --git a/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandParserMessages.js b/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandParserMessages.js index 4542b628b1..94ec2f639a 100644 --- a/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandParserMessages.js +++ b/src/assets/scripts/client/parsers/aircraftCommandParser/aircraftCommandParserMessages.js @@ -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`, diff --git a/src/assets/scripts/client/parsers/aircraftCommandParser/argumentParsers.js b/src/assets/scripts/client/parsers/aircraftCommandParser/argumentParsers.js index 0657180ffc..c9d36770bf 100644 --- a/src/assets/scripts/client/parsers/aircraftCommandParser/argumentParsers.js +++ b/src/assets/scripts/client/parsers/aircraftCommandParser/argumentParsers.js @@ -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]; }; diff --git a/src/assets/scripts/client/parsers/aircraftCommandParser/argumentValidators.js b/src/assets/scripts/client/parsers/aircraftCommandParser/argumentValidators.js index 8c28f2d49b..2afb7b1edf 100644 --- a/src/assets/scripts/client/parsers/aircraftCommandParser/argumentValidators.js +++ b/src/assets/scripts/client/parsers/aircraftCommandParser/argumentValidators.js @@ -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 * @@ -311,7 +324,7 @@ export const squawkValidator = (args = []) => { * * ``` * Allowed argument shapes: - * - ['dumba', '120'] + * - ['dumba', 'a120', 's210'] * ``` * * @function crossingValidator @@ -319,20 +332,51 @@ export const squawkValidator = (args = []) => { * @return {array} */ 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; + } } }; diff --git a/test/parsers/aircraftCommandParser/argumentParser.spec.js b/test/parsers/aircraftCommandParser/argumentParser.spec.js index 8bff4cfb9e..4b118e0eb0 100644 --- a/test/parsers/aircraftCommandParser/argumentParser.spec.js +++ b/test/parsers/aircraftCommandParser/argumentParser.spec.js @@ -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); }); diff --git a/test/parsers/aircraftCommandParser/argumentValidator.spec.js b/test/parsers/aircraftCommandParser/argumentValidator.spec.js index fd64cfa98e..1297f74cb6 100644 --- a/test/parsers/aircraftCommandParser/argumentValidator.spec.js +++ b/test/parsers/aircraftCommandParser/argumentValidator.spec.js @@ -350,44 +350,58 @@ ava('.squawkValidator() returns string when passed invalid squawk', t => { ava('.crossingValidator() returns a string when passed the wrong number of arguments', t => { let result = crossingValidator(); - t.true(result === 'Invalid argument length. Expected exactly two arguments'); + t.true(result === 'Invalid argument length. Expected two or three arguments'); result = crossingValidator([]); - t.true(result === 'Invalid argument length. Expected exactly two arguments'); + t.true(result === 'Invalid argument length. Expected two or three arguments'); - result = crossingValidator(['', '', '']); - t.true(result === 'Invalid argument length. Expected exactly two arguments'); + result = crossingValidator(['', '', '', '', '']); + t.true(result === 'Invalid argument length. Expected two or three arguments'); }); ava('.crossingValidator() returns undefined when passed valid arguments', t => { - let result = crossingValidator(['LEMDY', '50']); + let result = crossingValidator(['LEMDY', 'a50', 's210']); t.true(typeof result === 'undefined'); - result = crossingValidator(['BLUB', '100']); + result = crossingValidator(['BLUB', 'a100', 's250']); t.true(typeof result === 'undefined'); }); ava('.crossingValidator() returns an error when fixname is not a string', t => { - let result = crossingValidator([50, '50']); + let result = crossingValidator([50, 'a70', 's210']); t.true(result === 'Invalid argument. Must be a string'); - result = crossingValidator([{}, '100']); + result = crossingValidator([{}, 'a70', 's210']); t.true(result === 'Invalid argument. Must be a string'); - result = crossingValidator([[], '100']); + result = crossingValidator([[], 'a70', 's210']); t.true(result === 'Invalid argument. Must be a string'); }); ava('.crossingValidator() returns an error when altitude is not a number', t => { - let result = crossingValidator(['LEMDY', 'xx']); + let result = crossingValidator(['LEMDY', 'xx', 's210']); t.true(result === 'Invalid argument. Altitude must be a number'); - result = crossingValidator(['LEMDY', '']); + result = crossingValidator(['LEMDY', '', 's210']); t.true(result === 'Invalid argument. Altitude must be a number'); - result = crossingValidator(['LEMDY', []]); + result = crossingValidator(['LEMDY', [], 's210']); t.true(result === 'Invalid argument. Altitude must be a number'); - result = crossingValidator(['LEMDY', {}]); + result = crossingValidator(['LEMDY', {}, 's210']); t.true(result === 'Invalid argument. Altitude must be a number'); }); + +ava('.crossingValidator() returns an error when speed is not a number', t => { + let result = crossingValidator(['LEMDY', 'a70', 'xx']); + t.true(result === 'Invalid argument. Speed must be a number'); + + result = crossingValidator(['LEMDY', 'a70', '']); + t.true(result === 'Invalid argument. Speed must be a number'); + + result = crossingValidator(['LEMDY', 'a70', []]); + t.true(result === 'Invalid argument. Speed must be a number'); + + result = crossingValidator(['LEMDY', 'a70', {}]); + t.true(result === 'Invalid argument. Speed must be a number'); +});