From aacebaf4b8f101dbad17f20e5136f50cd6991a6f Mon Sep 17 00:00:00 2001 From: durk <durk> Date: Mon, 12 May 2008 10:07:41 +0000 Subject: [PATCH] Merging David Luff's AI/ATC code with AIModels. Part 1: - Move Dave's original code to a new directory (ATCDCL) so we can recycle the original ATC directory for generic ATC functions. --- src/ATCDCL/AIEntity.cxx | 79 + src/ATCDCL/AIEntity.hxx | 72 + src/ATCDCL/AIGAVFRTraffic.cxx | 463 ++++++ src/ATCDCL/AIGAVFRTraffic.hxx | 126 ++ src/ATCDCL/AILocalTraffic.cxx | 1596 ++++++++++++++++++ src/ATCDCL/AILocalTraffic.hxx | 233 +++ src/ATCDCL/AIMgr.cxx | 624 ++++++++ src/ATCDCL/AIMgr.hxx | 139 ++ src/ATCDCL/AIPlane.cxx | 272 ++++ src/ATCDCL/AIPlane.hxx | 165 ++ src/ATCDCL/ATC.cxx | 291 ++++ src/ATCDCL/ATC.hxx | 308 ++++ src/ATCDCL/ATCDialog.cxx | 424 +++++ src/ATCDCL/ATCDialog.hxx | 115 ++ src/ATCDCL/ATCProjection.cxx | 118 ++ src/ATCDCL/ATCProjection.hxx | 73 + src/ATCDCL/ATCVoice.cxx | 191 +++ src/ATCDCL/ATCVoice.hxx | 87 + src/ATCDCL/ATCmgr.cxx | 723 +++++++++ src/ATCDCL/ATCmgr.hxx | 212 +++ src/ATCDCL/ATCutils.cxx | 314 ++++ src/ATCDCL/ATCutils.hxx | 103 ++ src/ATCDCL/Makefile.am | 23 + src/ATCDCL/approach.cxx | 759 +++++++++ src/ATCDCL/approach.hxx | 231 +++ src/ATCDCL/atis.cxx | 225 +++ src/ATCDCL/atis.hxx | 95 ++ src/ATCDCL/commlist.cxx | 346 ++++ src/ATCDCL/commlist.hxx | 140 ++ src/ATCDCL/ground.cxx | 732 +++++++++ src/ATCDCL/ground.hxx | 367 +++++ src/ATCDCL/tower.cxx | 2671 +++++++++++++++++++++++++++++++ src/ATCDCL/tower.hxx | 365 +++++ src/ATCDCL/transmission.cxx | 100 ++ src/ATCDCL/transmission.hxx | 169 ++ src/ATCDCL/transmissionlist.cxx | 263 +++ src/ATCDCL/transmissionlist.hxx | 82 + 37 files changed, 13296 insertions(+) create mode 100644 src/ATCDCL/AIEntity.cxx create mode 100644 src/ATCDCL/AIEntity.hxx create mode 100644 src/ATCDCL/AIGAVFRTraffic.cxx create mode 100644 src/ATCDCL/AIGAVFRTraffic.hxx create mode 100644 src/ATCDCL/AILocalTraffic.cxx create mode 100644 src/ATCDCL/AILocalTraffic.hxx create mode 100644 src/ATCDCL/AIMgr.cxx create mode 100644 src/ATCDCL/AIMgr.hxx create mode 100644 src/ATCDCL/AIPlane.cxx create mode 100644 src/ATCDCL/AIPlane.hxx create mode 100644 src/ATCDCL/ATC.cxx create mode 100644 src/ATCDCL/ATC.hxx create mode 100644 src/ATCDCL/ATCDialog.cxx create mode 100644 src/ATCDCL/ATCDialog.hxx create mode 100644 src/ATCDCL/ATCProjection.cxx create mode 100644 src/ATCDCL/ATCProjection.hxx create mode 100644 src/ATCDCL/ATCVoice.cxx create mode 100644 src/ATCDCL/ATCVoice.hxx create mode 100644 src/ATCDCL/ATCmgr.cxx create mode 100644 src/ATCDCL/ATCmgr.hxx create mode 100644 src/ATCDCL/ATCutils.cxx create mode 100644 src/ATCDCL/ATCutils.hxx create mode 100644 src/ATCDCL/Makefile.am create mode 100644 src/ATCDCL/approach.cxx create mode 100644 src/ATCDCL/approach.hxx create mode 100644 src/ATCDCL/atis.cxx create mode 100644 src/ATCDCL/atis.hxx create mode 100644 src/ATCDCL/commlist.cxx create mode 100644 src/ATCDCL/commlist.hxx create mode 100644 src/ATCDCL/ground.cxx create mode 100644 src/ATCDCL/ground.hxx create mode 100644 src/ATCDCL/tower.cxx create mode 100644 src/ATCDCL/tower.hxx create mode 100644 src/ATCDCL/transmission.cxx create mode 100644 src/ATCDCL/transmission.hxx create mode 100644 src/ATCDCL/transmissionlist.cxx create mode 100644 src/ATCDCL/transmissionlist.hxx diff --git a/src/ATCDCL/AIEntity.cxx b/src/ATCDCL/AIEntity.cxx new file mode 100644 index 000000000..41baf4a05 --- /dev/null +++ b/src/ATCDCL/AIEntity.cxx @@ -0,0 +1,79 @@ +// FGAIEntity - abstract base class an artificial intelligence entity +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +/***************************************************************** +* +* WARNING - Curt has some ideas about AI traffic so anything in here +* may get rewritten or scrapped. Contact Curt http://www.flightgear.org/~curt +* before spending any time or effort on this code!!! +* +******************************************************************/ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <Main/globals.hxx> +#include <Scenery/scenery.hxx> +#include <simgear/constants.h> +#include <simgear/math/point3d.hxx> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/misc/sg_path.hxx> +#include <string> + +#include "AIEntity.hxx" + +FGAIEntity::FGAIEntity() { +} + +FGAIEntity::~FGAIEntity() { + //cout << "FGAIEntity dtor called..." << endl; + //cout << "Removing model from scene graph..." << endl; + globals->get_scenery()->get_scene_graph()->removeChild(_aip.getSceneGraph()); + + //cout << "Done!" << endl; +} + +void FGAIEntity::SetModel(osg::Node* model) { + _model = model; + _aip.init(_model.get()); + _aip.setVisible(false); + globals->get_scenery()->get_scene_graph()->addChild(_aip.getSceneGraph()); + +} + +void FGAIEntity::Update(double dt) { +} + +const string &FGAIEntity::GetCallsign() { + static string s = ""; + return(s); +} + +void FGAIEntity::RegisterTransmission(int code) { +} + +// Run the internal calculations +//void FGAIEntity::Update() { +void FGAIEntity::Transform() { + _aip.setPosition(_pos.lon(), _pos.lat(), _pos.elev() * SG_METER_TO_FEET); + _aip.setOrientation(_roll, _pitch, _hdg); + _aip.update(); +} diff --git a/src/ATCDCL/AIEntity.hxx b/src/ATCDCL/AIEntity.hxx new file mode 100644 index 000000000..1b184a833 --- /dev/null +++ b/src/ATCDCL/AIEntity.hxx @@ -0,0 +1,72 @@ +// FGAIEntity - abstract base class an artificial intelligence entity +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_AIEntity_HXX +#define _FG_AIEntity_HXX + +#include <simgear/math/point3d.hxx> +#include <simgear/scene/model/placement.hxx> + + +/***************************************************************** +* +* FGAIEntity - this class implements the minimum requirement +* for any AI entity - a position, an orientation, an associated +* 3D model, and the ability to be moved. It does nothing useful +* and all AI entities are expected to be derived from it. +* +******************************************************************/ +class FGAIEntity { + +public: + + FGAIEntity(); + virtual ~FGAIEntity(); + + // Set the 3D model to use (Must be called) + void SetModel(osg::Node* model); + + // Run the internal calculations + virtual void Update(double dt)=0; + + // Send a transmission *TO* the AIEntity. + // FIXME int code is a hack - eventually this will receive Alexander's coded messages. + virtual void RegisterTransmission(int code)=0; + + inline const Point3D& GetPos() const { return(_pos); } + + virtual const string& GetCallsign()=0; + +protected: + + Point3D _pos; // WGS84 lat & lon in degrees, elev above sea-level in meters + double _hdg; //True heading in degrees + double _roll; //degrees + double _pitch; //degrees + + char* _model_path; //Path to the 3D model + osg::ref_ptr<osg::Node> _model; // Pointer to the model + SGModelPlacement _aip; + + void Transform(); +}; + +#endif // _FG_AIEntity_HXX + diff --git a/src/ATCDCL/AIGAVFRTraffic.cxx b/src/ATCDCL/AIGAVFRTraffic.cxx new file mode 100644 index 000000000..6941a2489 --- /dev/null +++ b/src/ATCDCL/AIGAVFRTraffic.cxx @@ -0,0 +1,463 @@ +// FGAILocalTraffic - AIEntity derived class with enough logic to +// fly and interact with the traffic pattern. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <Airports/runways.hxx> +#include <Main/globals.hxx> +#include <simgear/math/point3d.hxx> +#include <string> +#include <math.h> + +SG_USING_STD(string); + +#include "ATC.hxx" +#include "ATCmgr.hxx" +#include "AILocalTraffic.hxx" +#include "AIGAVFRTraffic.hxx" +#include "ATCutils.hxx" +#include "tower.hxx" + +FGAIGAVFRTraffic::FGAIGAVFRTraffic() { + ATC = globals->get_ATC_mgr(); + _towerContactedIncoming = false; + _clearedStraightIn = false; + _clearedDownwindEntry = false; + _incoming = false; + _straightIn = false; + _downwindEntry = false; + _climbout = false; + _local = false; + _established = false; + _e45 = false; + _entering = false; + _turning = false; + _cruise_climb_ias = 90.0; + _cruise_ias = 110.0; + patternDirection = -1.0; + + // TESTING - REMOVE OR COMMENT OUT BEFORE COMMIT!!! + //_towerContactPrinted = false; +} + +FGAIGAVFRTraffic::~FGAIGAVFRTraffic() { +} + +// We should never need to Init FGAIGAVFRTraffic in the pattern since that implies arrivel +// and we can just use an FGAILocalTraffic instance for that instead. + +// Init en-route to destID at point pt. +// TODO - no idea what to do if pt is above planes ceiling due mountains!! +bool FGAIGAVFRTraffic::Init(const Point3D& pt, const string& destID, const string& callsign) { + FGAILocalTraffic::Init(callsign, destID, EN_ROUTE); + // TODO FIXME - to get up and running we're going to ignore elev and get FGAIMgr to + // pass in known good values for the test location. Need to fix this!!! (or at least canonically decide who has responsibility for setting elev). + _enroute = true; + _destID = destID; + _pos = pt; + _destPos = fgGetAirportPos(destID); // TODO - check if we are within the tower catchment area already. + _cruise_alt = (_destPos.elev() + 2500.0) * SG_FEET_TO_METER; // TODO look at terrain elevation as well + _pos.setelev(_cruise_alt); + // initially set waypoint as airport location + _wp = _destPos; + // Set the initial track + track = GetHeadingFromTo(_pos, _wp); + // And set the plane to keep following it. + SetTrack(GetHeadingFromTo(_pos, _wp)); + _roll = 0.0; + _pitch = 0.0; + slope = 0.0; + // TODO - set climbout if altitude is below normal cruising altitude? + //Transform(); + // Assume it's OK to set the plane visible + _aip.setVisible(true); + //cout << "Setting visible true\n"; + Transform(); + return(true); +} + +// Init at srcID to fly to destID +bool FGAIGAVFRTraffic::Init(const string& srcID, const string& destID, const string& callsign, OperatingState state) { + _enroute = false; + FGAILocalTraffic::Init(callsign, srcID, PARKED); + return(true); +} + +void FGAIGAVFRTraffic::Update(double dt) { + if(_enroute) { + //cout << "_enroute\n"; + //cout << "e" << flush; + FlyPlane(dt); + //cout << "f" << flush; + Transform(); + //cout << "g" << flush; + FGAIPlane::Update(dt); + //cout << "h" << flush; + responseCounter += dt; + + // we shouldn't really need this since there's a LOD of 10K on the whole plane anyway I think. + // There are two _aip.setVisible statements set when _local = true that can be removed if the below is removed. + if(dclGetHorizontalSeparation(_pos, Point3D(fgGetDouble("/position/longitude-deg"), fgGetDouble("/position/latitude-deg"), 0.0)) > 8000) _aip.setVisible(false); + else _aip.setVisible(true); + + } else if(_local) { + //cout << "L"; + //cout << "_local\n"; + FGAILocalTraffic::Update(dt); + } +} + +void FGAIGAVFRTraffic::FlyPlane(double dt) { + if(_climbout) { + // Check whether to level off + if(_pos.elev() >= _cruise_alt) { + slope = 0.0; + _pitch = 0.0; + IAS = _cruise_ias; // FIXME - use smooth transistion to new speed and attitude. + _climbout = false; + } else { + slope = 4.0; + _pitch = 5.0; + IAS = _cruise_climb_ias; + } + } else { + // TESTING + /* + if(dclGetHorizontalSeparation(_destPos, _pos) / 1600.0 < 8.1) { + if(!_towerContactPrinted) { + if(airportID == "KSQL") { + cout << "****************************************************************\n"; + cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"; + cout << "****************************************************************\n"; + } + _towerContactPrinted = true; + } + } + */ + + // if distance to destination is less than 6 - 9 miles contact tower + // and prepare to become _incoming after response. + // Possibly check whether to start descent before this? + //cout << "." << flush; + //cout << "sep = " << dclGetHorizontalSeparation(_destPos, _pos) / 1600.0 << '\n'; + if(dclGetHorizontalSeparation(_destPos, _pos) / 1600.0 < 8.0) { + //cout << "-" << flush; + if(!_towerContactedIncoming) { + //cout << "_" << flush; + GetAirportDetails(airportID); + //cout << "L" << flush; + if(_controlled) { + freq = (double)tower->get_freq() / 100.0; + tuned_station = tower; + } else { + freq = 122.8; // TODO - need to get the correct CTAF/Unicom frequency if no tower + tuned_station = NULL; + } + //cout << "freq = " << freq << endl; + GetRwyDetails(airportID); + //"@AP Tower @CS @MI miles @CD of the airport for full stop with ATIS" + // At the bare minimum we ought to make sure it goes the right way at dual parallel rwy airports! + if(rwy.rwyID.size() == 3) { + patternDirection = (rwy.rwyID.substr(2,1) == "R" ? 1 : -1); + } + if(_controlled) { + pending_transmission = tower->get_name(); + pending_transmission += " Tower "; + } else { + pending_transmission = "Traffic "; + // TODO - find some way of getting uncontrolled airport name + } + pending_transmission += plane.callsign; + //char buf[10]; + int dist_miles = (int)dclGetHorizontalSeparation(_pos, _destPos) / 1600; + //sprintf(buf, " %i ", dist_miles); + pending_transmission += " "; + pending_transmission += ConvertNumToSpokenDigits(dist_miles); + if(dist_miles > 1) pending_transmission += " miles "; + else pending_transmission += " mile "; + pending_transmission += GetCompassDirection(GetHeadingFromTo(_destPos, _pos)); + pending_transmission += " of the airport for full stop with ATIS"; + //cout << pending_transmission << endl; + Transmit(14); // 14 is the callback code, NOT the timeout! + responseCounter = 0; + _towerContactedIncoming = true; + } else { + //cout << "?" << flush; + if(_clearedStraightIn && responseCounter > 5.5) { + //cout << "5 " << flush; + _clearedStraightIn = false; + _straightIn = true; + _incoming = true; + _wp = GetPatternApproachPos(); + //_hdg = GetHeadingFromTo(_pos, _wp); // TODO - turn properly! + SetTrack(GetHeadingFromTo(_pos, _wp)); + slope = atan((_wp.elev() - _pos.elev()) / dclGetHorizontalSeparation(_wp, _pos)) * DCL_RADIANS_TO_DEGREES; + double thesh_offset = 0.0; + Point3D opos = ortho.ConvertToLocal(_pos); + double angToApt = atan((_pos.elev() - fgGetAirportElev(airportID)) / (opos.y() - thesh_offset)) * DCL_RADIANS_TO_DEGREES; + //cout << "angToApt = " << angToApt << ' '; + slope = (angToApt > -5.0 ? 0.0 : angToApt); + //cout << "slope = " << slope << '\n'; + pending_transmission = "Straight-in "; + pending_transmission += ConvertRwyNumToSpokenString(rwy.rwyID); + pending_transmission += " "; + pending_transmission += plane.callsign; + //cout << pending_transmission << '\n'; + ConditionalTransmit(4); + } else if(_clearedDownwindEntry && responseCounter > 5.5) { + //cout << "6" << flush; + _clearedDownwindEntry = false; + _downwindEntry = true; + _incoming = true; + _wp = GetPatternApproachPos(); + SetTrack(GetHeadingFromTo(_pos, _wp)); + slope = atan((_wp.elev() - _pos.elev()) / dclGetHorizontalSeparation(_wp, _pos)) * DCL_RADIANS_TO_DEGREES; + //cout << "slope = " << slope << '\n'; + pending_transmission = "Report "; + pending_transmission += (patternDirection == 1 ? "right downwind " : "left downwind "); + pending_transmission += ConvertRwyNumToSpokenString(rwy.rwyID); + pending_transmission += " "; + pending_transmission += plane.callsign; + //cout << pending_transmission << '\n'; + ConditionalTransmit(4); + } + } + if(_pos.elev() < (fgGetAirportElev(airportID) + (1000.0 * SG_FEET_TO_METER))) slope = 0.0; + } + } + if(_incoming) { + //cout << "i" << '\n'; + Point3D orthopos = ortho.ConvertToLocal(_pos); + // TODO - Check whether to start descent + // become _local after the 3 mile report. + if(_pos.elev() < (fgGetAirportElev(airportID) + (1000.0 * SG_FEET_TO_METER))) slope = 0.0; + // TODO - work out why I needed to add the above line to stop the plane going underground!!! + // (Although it's worth leaving it in as a robustness check anyway). + if(_straightIn) { + //cout << "A " << flush; + if(fabs(orthopos.x()) < 10.0 && !_established) { + SetTrack(rwy.hdg); + _established = true; + //cout << "Established at " << orthopos << '\n'; + } + double thesh_offset = 30.0; + //cout << "orthopos.y = " << orthopos.y() << " alt = " << _pos.elev() - fgGetAirportElev(airportID) << '\n'; + if(_established && (orthopos.y() > -5400.0)) { + slope = atan((_pos.elev() - fgGetAirportElev(airportID)) / (orthopos.y() - thesh_offset)) * DCL_RADIANS_TO_DEGREES; + //cout << "slope0 = " << slope << '\n'; + } + //cout << "slope1 = " << slope << '\n'; + if(slope > -5.5) slope = 0.0; // ie we're too low. + //cout << "slope2 = " << slope << '\n'; + slope += 0.001; // To avoid yo-yoing with the above. + //if(_established && (orthopos.y() > -5400.0)) slope = -5.5; + if(_established && (orthopos.y() > -4800.0)) { + pending_transmission = "3 mile final Runway "; + pending_transmission += ConvertRwyNumToSpokenString(rwy.rwyID); + pending_transmission += " "; + pending_transmission += plane.callsign; + //cout << pending_transmission << '\n'; + ConditionalTransmit(35); + _local = true; + _aip.setVisible(true); // HACK + _enroute = false; + StraightInEntry(true); + } + } else if(_downwindEntry) { + //cout << "B" << flush; + if(_entering) { + //cout << "C" << flush; + if(_turning) { + if(fabs(_hdg - (rwy.hdg + 180)) < 2.0) { // TODO - use track instead of _hdg? + //cout << "Going Local...\n"; + leg = DOWNWIND; + _local = true; + _aip.setVisible(true); // HACK + _enroute = false; + _entering = false; + _turning = false; + DownwindEntry(); + } + } + if(fabs(orthopos.x() - (patternDirection == 1 ? 1000 : -1000)) < (_e45 ? 175 : 550)) { // Caution - hardwired turn clearances. + //cout << "_turning...\n"; + _turning = true; + SetTrack(rwy.hdg + 180.0); + } // TODO - need to check for other traffic in the pattern and enter much more integilently than that!!! + } else { + //cout << "D" << flush; + //cout << '\n' << dclGetHorizontalSeparation(_wp, _pos) << '\n'; + //cout << ortho.ConvertToLocal(_pos); + //cout << ortho.ConvertToLocal(_wp); + if(dclGetHorizontalSeparation(_wp, _pos) < 100.0) { + pending_transmission = "2 miles out for "; + pending_transmission += (patternDirection == 1 ? "right " : "left "); + pending_transmission += "downwind Runway "; + pending_transmission += ConvertRwyNumToSpokenString(rwy.rwyID); + pending_transmission += " "; + pending_transmission += plane.callsign; + //cout << pending_transmission << '\n'; + // TODO - are we at pattern altitude?? + slope = 0.0; + ConditionalTransmit(30); + if(_e45) { + SetTrack(patternDirection == 1 ? rwy.hdg - 135.0 : rwy.hdg + 135.0); + } else { + SetTrack(patternDirection == 1 ? rwy.hdg + 90.0 : rwy.hdg - 90.0); + } + //if(_hdg < 0.0) _hdg += 360.0; + _entering = true; + } else { + SetTrack(GetHeadingFromTo(_pos, _wp)); + } + } + } + } else { + // !_incoming + slope = 0.0; + } + // FIXME - lots of hackery in the next six lines!!!! + //double track = _hdg; + double crab = 0.0; // This is a placeholder for when we take wind into account. + _hdg = track + crab; + double vel = _cruise_ias; + double dist = vel * 0.514444 * dt; + _pos = dclUpdatePosition(_pos, track, slope, dist); +} + +void FGAIGAVFRTraffic::RegisterTransmission(int code) { + switch(code) { + case 1: // taxi request cleared + FGAILocalTraffic::RegisterTransmission(code); + break; + case 2: // contact tower + FGAILocalTraffic::RegisterTransmission(code); + break; + case 3: // Cleared to line up + FGAILocalTraffic::RegisterTransmission(code); + break; + case 4: // cleared to take-off + FGAILocalTraffic::RegisterTransmission(code); + break; + case 5: // contact ground + FGAILocalTraffic::RegisterTransmission(code); + break; + case 6: // taxi to the GA parking + FGAILocalTraffic::RegisterTransmission(code); + break; + case 7: // Cleared to land + FGAILocalTraffic::RegisterTransmission(code); + break; + case 13: // Go around! + FGAILocalTraffic::RegisterTransmission(code); + break; + case 14: // VFR approach for straight-in + responseCounter = 0; + _clearedStraightIn = true; + break; + case 15: // VFR approach for downwind entry + responseCounter = 0; + _clearedDownwindEntry = true; + break; + default: + SG_LOG(SG_ATC, SG_WARN, "FGAIGAVFRTraffic::RegisterTransmission(...) called with unknown code " << code); + FGAILocalTraffic::RegisterTransmission(code); + break; + } +} + +// Callback handler +// TODO - Really should enumerate these coded values. +void FGAIGAVFRTraffic::ProcessCallback(int code) { + // 1 - Request Departure from ground + // 2 - Report at hold short + // 10 - report crosswind + // 11 - report downwind + // 12 - report base + // 13 - report final + // 14 - Contact Tower for VFR arrival + // 99 - Remove self + if(code < 14) { + FGAILocalTraffic::ProcessCallback(code); + } else if(code == 14) { + if(_controlled) { + tower->VFRArrivalContact(plane, this, FULL_STOP); + } + // TODO else possibly announce arrival intentions at uncontrolled airport? + } else if(code == 99) { + // Might handle this different in future - hence separated from the other codes to pass to AILocalTraffic. + FGAILocalTraffic::ProcessCallback(code); + } +} + +// Return an appropriate altitude to fly at based on the desired altitude and direction +// whilst respecting the quadrangle rule. +int FGAIGAVFRTraffic::GetQuadrangleAltitude(int dir, int des_alt) { + return(8888); + // TODO - implement me! +} + +// Calculates the position needed to set up for either pattern entry or straight in approach. +// Currently returns one of three positions dependent on initial position wrt threshold of active rwy. +// 1/ A few miles out on extended centreline for straight-in. +// 2/ At an appropriate point on circuit side of rwy for a 45deg entry to downwind. +// 3/ At and appropriate point on non-circuit side of rwy at take-off end for perpendicular entry to circuit overflying end-of-rwy. +Point3D FGAIGAVFRTraffic::GetPatternApproachPos() { + //cout << "\n\n"; + //cout << "Calculating pattern approach pos for " << plane.callsign << '\n'; + Point3D orthopos = ortho.ConvertToLocal(_pos); + Point3D tmp; + //cout << "patternDirection = " << patternDirection << '\n'; + if(orthopos.y() >= -1000.0) { // Note that this has to be set the same as the calculation in tower.cxx - at the moment approach type is not transmitted properly between the two. + //cout << "orthopos.x = " << orthopos.x() << '\n'; + if((orthopos.x() * patternDirection) > 0.0) { // 45 deg entry + tmp.setx(2000 * patternDirection); + tmp.sety((rwy.end2ortho.y() / 2.0) + 2000); + tmp.setelev(fgGetAirportElev(airportID) + (1000 * SG_FEET_TO_METER)); + _e45 = true; + //cout << "45 deg entry... "; + } else { + tmp.setx(1000 * patternDirection * -1); + tmp.sety(rwy.end2ortho.y()); + tmp.setelev(fgGetAirportElev(airportID) + (1000 * SG_FEET_TO_METER)); + _e45 = false; + //cout << "90 deg entry... "; + } + } else { + tmp.setx(0); + tmp.sety(-5400); + tmp.setelev((5400.0 / 6.0) + fgGetAirportElev(airportID) + 10.0); + //cout << "Straight in... "; + } + //cout << "Waypoint is " << tmp << '\n'; + //cout << ortho.ConvertFromLocal(tmp) << '\n'; + //cout << '\n'; + //exit(-1); + return ortho.ConvertFromLocal(tmp); +} + +//FGAIGAVFRTraffic:: + +//FGAIGAVFRTraffic:: + +//FGAIGAVFRTraffic:: diff --git a/src/ATCDCL/AIGAVFRTraffic.hxx b/src/ATCDCL/AIGAVFRTraffic.hxx new file mode 100644 index 000000000..9aabd8746 --- /dev/null +++ b/src/ATCDCL/AIGAVFRTraffic.hxx @@ -0,0 +1,126 @@ +// FGAILocalTraffic - AIEntity derived class with enough logic to +// fly and interact with the traffic pattern. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_AIGAVFRTraffic_HXX +#define _FG_AIGAVFRTraffic_HXX + +#include <simgear/math/point3d.hxx> +#include <Main/fg_props.hxx> + +#include "AILocalTraffic.hxx" + +#include <string> +SG_USING_STD(string); + +class FGAIGAVFRTraffic : public FGAILocalTraffic { + +public: + + FGAIGAVFRTraffic(); + ~FGAIGAVFRTraffic(); + + // Init en-route to destID at point pt. (lat, lon, elev) (elev in meters, lat and lon in degrees). + bool Init(const Point3D& pt, const string& destID, const string& callsign); + // Init at srcID to fly to destID + bool Init(const string& srcID, const string& destID, const string& callsign, OperatingState state = PARKED); + + // Run the internal calculations + void Update(double dt); + + // Return what type of landing we're doing on this circuit + //LandingType GetLandingOption(); + + void RegisterTransmission(int code); + + // Process callbacks sent by base class + // (These codes are not related to the codes above) + void ProcessCallback(int code); + +protected: + + // Do what is necessary to land and parkup at home airport + void ReturnToBase(double dt); + + //void GetRwyDetails(string id); + + +private: + FGATCMgr* ATC; + // This is purely for synactic convienience to avoid writing globals->get_ATC_mgr()-> all through the code! + + // High-level stuff + OperatingState operatingState; + bool touchAndGo; //True if circuits should be flown touch and go, false for full stop + + // Performance characteristics of the plane in knots and ft/min - some of this might get moved out into FGAIPlane + double best_rate_of_climb_speed; + double best_rate_of_climb; + double nominal_climb_speed; + double nominal_climb_rate; + double nominal_cruise_speed; + double nominal_circuit_speed; + double nominal_descent_rate; + double nominal_approach_speed; + double nominal_final_speed; + double stall_speed_landing_config; + + // environment - some of this might get moved into FGAIPlane + SGPropertyNode_ptr wind_from_hdg; //degrees + SGPropertyNode_ptr wind_speed_knots; //knots + + atc_type changeFreqType; // the service we need to change to + + void CalculateSoD(double base_leg_pos, double downwind_leg_pos, bool pattern_direction); + + // GA VFR specific + bool _towerContactedIncoming; + bool _straightIn; + bool _clearedStraightIn; + bool _downwindEntry; + bool _clearedDownwindEntry; + Point3D _wp; // Next waypoint (ie. the one we're currently heading for) + bool _enroute; + string _destID; + bool _climbout; + double _cruise_alt; + double _cruise_ias; + double _cruise_climb_ias; + Point3D _destPos; + bool _local; + bool _incoming; + bool _established; + bool _e45; + bool _entering; + bool _turning; + + //ssgBranch* _model; + + int GetQuadrangleAltitude(int dir, int des_alt); + + Point3D GetPatternApproachPos(); + + void FlyPlane(double dt); + + // HACK for testing - remove or comment out before CVS commit!!! + //bool _towerContactPrinted; +}; + +#endif // _FG_AILocalTraffic_HXX diff --git a/src/ATCDCL/AILocalTraffic.cxx b/src/ATCDCL/AILocalTraffic.cxx new file mode 100644 index 000000000..15eb6a3d3 --- /dev/null +++ b/src/ATCDCL/AILocalTraffic.cxx @@ -0,0 +1,1596 @@ +// FGAILocalTraffic - AIEntity derived class with enough logic to +// fly and interact with the traffic pattern. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +/*========================================================== + +TODO list. + +Should get pattern direction from tower. + +Need to continually monitor and adjust deviation from glideslope +during descent to avoid occasionally landing short or long. + +============================================================*/ + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/scene/model/location.hxx> + +#include <Airports/runways.hxx> +#include <Main/globals.hxx> +#include <Main/viewer.hxx> +#include <Scenery/scenery.hxx> +#include <Scenery/tilemgr.hxx> +#include <simgear/math/point3d.hxx> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/misc/sg_path.hxx> +#include <string> +#include <math.h> + +SG_USING_STD(string); + +#include "ATCmgr.hxx" +#include "AILocalTraffic.hxx" +#include "ATCutils.hxx" +#include "AIMgr.hxx" + +FGAILocalTraffic::FGAILocalTraffic() { + /*ssgBranch *model = sgLoad3DModel( globals->get_fg_root(), + planepath.c_str(), + globals->get_props(), + globals->get_sim_time_sec() ); + *//* + _model = model; + _aip.init(_model); + */ + //SetModel(model); + + ATC = globals->get_ATC_mgr(); + + // TODO - unhardwire this + plane.type = GA_SINGLE; + + _roll = 0.0; + _pitch = 0.0; + _hdg = 270.0; + + //Hardwire initialisation for now - a lot of this should be read in from config eventually + Vr = 70.0; + best_rate_of_climb_speed = 70.0; + //best_rate_of_climb; + //nominal_climb_speed; + //nominal_climb_rate; + //nominal_circuit_speed; + //min_circuit_speed; + //max_circuit_speed; + nominal_descent_rate = 500.0; + nominal_final_speed = 65.0; + //nominal_approach_speed; + //stall_speed_landing_config; + nominalTaxiSpeed = 7.5; + taxiTurnRadius = 8.0; + wheelOffset = 1.45; // Warning - hardwired to the C172 - we need to read this in from file. + elevInitGood = false; + // Init the property nodes + wind_from_hdg = fgGetNode("/environment/wind-from-heading-deg", true); + wind_speed_knots = fgGetNode("/environment/wind-speed-kt", true); + circuitsToFly = 0; + liningUp = false; + taxiRequestPending = false; + taxiRequestCleared = false; + holdingShort = false; + clearedToLineUp = false; + clearedToTakeOff = false; + _clearedToLand = false; + reportReadyForDeparture = false; + contactTower = false; + contactGround = false; + _taxiToGA = false; + _removeSelf = false; + + descending = false; + targetDescentRate = 0.0; + goAround = false; + goAroundCalled = false; + + transmitted = false; + + freeTaxi = false; + _savedSlope = 0.0; + + _controlled = false; + + _invisible = false; +} + +FGAILocalTraffic::~FGAILocalTraffic() { +} + +void FGAILocalTraffic::GetAirportDetails(const string& id) { + AirportATC a; + if(ATC->GetAirportATCDetails(airportID, &a)) { + if(a.tower_freq) { // Has a tower - TODO - check the opening hours!!! + tower = (FGTower*)ATC->GetATCPointer(airportID, TOWER); + if(tower == NULL) { + // Something has gone wrong - abort or carry on with un-towered operation? + SG_LOG(SG_ATC, SG_ALERT, "ERROR - can't get a tower pointer from tower control for " << airportID << " in FGAILocalTraffic::GetAirportDetails() :-("); + _controlled = false; + } else { + _controlled = true; + } + if(tower) { + ground = tower->GetGroundPtr(); + if(ground == NULL) { + // Something has gone wrong :-( + SG_LOG(SG_ATC, SG_ALERT, "ERROR - can't get a ground pointer from tower control in FGAILocalTraffic::GetAirportDetails() :-("); + } + } + } else { + _controlled = false; + // TODO - Check CTAF, unicom etc + } + } else { + SG_LOG(SG_ATC, SG_ALERT, "Unable to find airport details in for " << airportID << " in FGAILocalTraffic::GetAirportDetails() :-("); + _controlled = false; + } + // Get the airport elevation + aptElev = fgGetAirportElev(airportID.c_str()); + //cout << "Airport elev in AILocalTraffic = " << aptElev << '\n'; + // WARNING - we use this elev for the whole airport - some assumptions in the code + // might fall down with very slopey airports. +} + +// Get details of the active runway +// It is assumed that by the time this is called the tower control and airport code will have been set up. +void FGAILocalTraffic::GetRwyDetails(const string& id) { + //cout << "GetRwyDetails called" << endl; + + if(_controlled) { + rwy.rwyID = tower->GetActiveRunway(); + } else { + // TODO - get a proper runway ID from uncontrolled airports + rwy.rwyID = "00"; + } + + // Now we need to get the threshold position and rwy heading + + FGRunway runway; + bool rwyGood = globals->get_runways()->search(id, rwy.rwyID, &runway); + if(rwyGood) { + double hdg = runway._heading; + double other_way = hdg - 180.0; + while(other_way <= 0.0) { + other_way += 360.0; + } + + // move to the +l end/center of the runway + //cout << "Runway center is at " << runway._lon << ", " << runway._lat << '\n'; + Point3D origin = Point3D(runway._lon, runway._lat, aptElev); + Point3D ref = origin; + double tshlon, tshlat, tshr; + double tolon, tolat, tor; + rwy.length = runway._length * SG_FEET_TO_METER; + rwy.width = runway._width * SG_FEET_TO_METER; + geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), other_way, + rwy.length / 2.0 - 25.0, &tshlat, &tshlon, &tshr ); + geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), hdg, + rwy.length / 2.0 - 25.0, &tolat, &tolon, &tor ); + // Note - 25 meters in from the runway end is a bit of a hack to put the plane ahead of the user. + // now copy what we need out of runway into rwy + rwy.threshold_pos = Point3D(tshlon, tshlat, aptElev); + Point3D takeoff_end = Point3D(tolon, tolat, aptElev); + //cout << "Threshold position = " << tshlon << ", " << tshlat << ", " << aptElev << '\n'; + //cout << "Takeoff position = " << tolon << ", " << tolat << ", " << aptElev << '\n'; + rwy.hdg = hdg; + // Set the projection for the local area + //cout << "Initing ortho for airport " << id << '\n'; + ortho.Init(rwy.threshold_pos, rwy.hdg); + rwy.end1ortho = ortho.ConvertToLocal(rwy.threshold_pos); // should come out as zero + rwy.end2ortho = ortho.ConvertToLocal(takeoff_end); + } else { + SG_LOG(SG_ATC, SG_ALERT, "Help - can't get good runway at airport " << id << " in FGAILocalTraffic!!"); + } +} + + +/* +There are two possible scenarios during initialisation: +The first is that the user is flying towards the airport, and hence the traffic +could be initialised anywhere, as long as the AI planes are consistent with +each other. +The second is that the user has started the sim at or close to the airport, and +hence the traffic must be initialised with respect to the user as well as each other. +To a certain extent it's FGAIMgr that has to worry about this, but we need to provide +sufficient initialisation functionality within the plane classes to allow the manager +to initially position them where and how required. +*/ +bool FGAILocalTraffic::Init(const string& callsign, const string& ICAO, OperatingState initialState, PatternLeg initialLeg) { + //cout << "FGAILocalTraffic.Init(...) called" << endl; + airportID = ICAO; + + plane.callsign = callsign; + + if(initialState == EN_ROUTE) return(true); + + // Get the ATC pointers and airport elev + GetAirportDetails(airportID); + + // Get the active runway details (and copy them into rwy) + GetRwyDetails(airportID); + //cout << "Runway is " << rwy.rwyID << '\n'; + + // FIXME TODO - pattern direction is still hardwired + patternDirection = -1; // Left + // At the bare minimum we ought to make sure it goes the right way at dual parallel rwy airports! + if(rwy.rwyID.size() == 3) { + patternDirection = (rwy.rwyID.substr(2,1) == "R" ? 1 : -1); + } + + if(_controlled) { + if((initialState == PARKED) || (initialState == TAXIING)) { + freq = (double)ground->get_freq() / 100.0; + } else { + freq = (double)tower->get_freq() / 100.0; + } + } else { + freq = 122.8; + // TODO - find the proper freq if CTAF or unicom or after-hours. + } + + //cout << "In Init(), initialState = " << initialState << endl; + operatingState = initialState; + Point3D orthopos; + switch(operatingState) { + case PARKED: + tuned_station = ground; + ourGate = ground->GetGateNode(); + if(ourGate == NULL) { + // Implies no available gates - what shall we do? + // For now just vanish the plane - possibly we can make this more elegant in the future + SG_LOG(SG_ATC, SG_ALERT, "No gate found by FGAILocalTraffic whilst attempting Init at " << airportID << '\n'); + return(false); + } + _pitch = 0.0; + _roll = 0.0; + vel = 0.0; + slope = 0.0; + _pos = ourGate->pos; + _pos.setelev(aptElev); + _hdg = ourGate->heading; + Transform(); + + // Now we've set the position we can do the ground elev + elevInitGood = false; + inAir = false; + DoGroundElev(); + + break; + case TAXIING: + //tuned_station = ground; + // FIXME - implement this case properly + // For now we'll assume that the plane should start at the hold short in this case + // and that we're working without ground network elements. Ie. an airport with no facility file. + if(_controlled) { + tuned_station = tower; + } else { + tuned_station = NULL; + } + freeTaxi = true; + // Set a position and orientation in an approximate place for hold short. + //cout << "rwy.width = " << rwy.width << '\n'; + orthopos = Point3D((rwy.width / 2.0 + 10.0) * -1.0, 0.0, 0.0); + // TODO - set the x pos to be +ve if a RH parallel rwy. + _pos = ortho.ConvertFromLocal(orthopos); + _pos.setelev(aptElev); + _hdg = rwy.hdg + 90.0; + // TODO - reset the heading if RH rwy. + _pitch = 0.0; + _roll = 0.0; + vel = 0.0; + slope = 0.0; + elevInitGood = false; + inAir = false; + Transform(); + DoGroundElev(); + //Transform(); + + responseCounter = 0.0; + contactTower = false; + changeFreq = true; + holdingShort = true; + clearedToLineUp = false; + changeFreqType = TOWER; + + break; + case IN_PATTERN: + // For now we'll always start the in_pattern case on the threshold ready to take-off + // since we've got the implementation for this case already. + // TODO - implement proper generic in_pattern startup. + + // 18/10/03 - adding the ability to start on downwind (mainly to speed testing of the go-around code!!) + + //cout << "Starting in pattern...\n"; + + if(_controlled) { + tuned_station = tower; + } else { + tuned_station = NULL; + } + + circuitsToFly = 0; // ie just fly this circuit and then stop + touchAndGo = false; + + if(initialLeg == DOWNWIND) { + _pos = ortho.ConvertFromLocal(Point3D(1000*patternDirection, 800, 0.0)); + _pos.setelev(rwy.threshold_pos.elev() + 1000 * SG_FEET_TO_METER); + _hdg = rwy.hdg + 180.0; + leg = DOWNWIND; + elevInitGood = false; + inAir = true; + SetTrack(rwy.hdg - (180 * patternDirection)); + slope = 0.0; + _pitch = 0.0; + _roll = 0.0; + IAS = 90.0; + descending = false; + _aip.setVisible(true); + if(_controlled) { + tower->RegisterAIPlane(plane, this, CIRCUIT, DOWNWIND); + } + Transform(); + } else { + // Default to initial position on threshold for now + _pos.setlat(rwy.threshold_pos.lat()); + _pos.setlon(rwy.threshold_pos.lon()); + _pos.setelev(rwy.threshold_pos.elev()); + _hdg = rwy.hdg; + + // Now we've set the position we can do the ground elev + // This might not always be necessary if we implement in-air start + elevInitGood = false; + inAir = false; + + _pitch = 0.0; + _roll = 0.0; + leg = TAKEOFF_ROLL; + vel = 0.0; + slope = 0.0; + + Transform(); + DoGroundElev(); + } + + operatingState = IN_PATTERN; + + break; + case EN_ROUTE: + // This implies we're being init'd by AIGAVFRTraffic - simple return now + return(true); + default: + SG_LOG(SG_ATC, SG_ALERT, "Attempt to set unknown operating state in FGAILocalTraffic.Init(...)\n"); + return(false); + } + + + return(true); +} + + +// Set up downwind state - this is designed to be called from derived classes who are already tuned to tower +void FGAILocalTraffic::DownwindEntry() { + circuitsToFly = 0; // ie just fly this circuit and then stop + touchAndGo = false; + operatingState = IN_PATTERN; + leg = DOWNWIND; + elevInitGood = false; + inAir = true; + SetTrack(rwy.hdg - (180 * patternDirection)); + slope = 0.0; + _pitch = 0.0; + _roll = 0.0; + IAS = 90.0; + descending = false; +} + +void FGAILocalTraffic::StraightInEntry(bool des) { + //cout << "************ STRAIGHT-IN ********************\n"; + circuitsToFly = 0; // ie just fly this circuit and then stop + touchAndGo = false; + operatingState = IN_PATTERN; + leg = FINAL; + elevInitGood = false; + inAir = true; + SetTrack(rwy.hdg); + transmitted = true; // TODO - fix this hack. + // TODO - set up the next 5 properly for a descent! + slope = -5.5; + _pitch = 0.0; + _roll = 0.0; + IAS = 90.0; + descending = des; +} + + +// Return what type of landing we're doing on this circuit +LandingType FGAILocalTraffic::GetLandingOption() { + //cout << "circuitsToFly = " << circuitsToFly << '\n'; + if(circuitsToFly) { + return(touchAndGo ? TOUCH_AND_GO : STOP_AND_GO); + } else { + return(FULL_STOP); + } +} + + +// Commands to do something from higher level logic +void FGAILocalTraffic::FlyCircuits(int numCircuits, bool tag) { + //cout << "FlyCircuits called" << endl; + + switch(operatingState) { + case IN_PATTERN: + circuitsToFly += numCircuits; + return; + break; + case TAXIING: + // HACK - assume that we're taxiing out for now + circuitsToFly += numCircuits; + touchAndGo = tag; + break; + case PARKED: + circuitsToFly = numCircuits; // Note that one too many circuits gets flown because we only test and decrement circuitsToFly after landing + // thus flying one too many circuits. TODO - Need to sort this out better! + touchAndGo = tag; + break; + case EN_ROUTE: + break; + } +} + +// Run the internal calculations +void FGAILocalTraffic::Update(double dt) { + //cout << "U" << flush; + + // we shouldn't really need this since there's a LOD of 10K on the whole plane anyway I think. + // At the moment though I need to to avoid DList overflows - the whole plane LOD obviously isn't getting picked up. + if(!_invisible) { + if(dclGetHorizontalSeparation(_pos, Point3D(fgGetDouble("/position/longitude-deg"), fgGetDouble("/position/latitude-deg"), 0.0)) > 8000) _aip.setVisible(false); + else _aip.setVisible(true); + } else { + _aip.setVisible(false); + } + + //double responseTime = 10.0; // seconds - this should get more sophisticated at some point + responseCounter += dt; + if((contactTower) && (responseCounter >= 8.0)) { + // Acknowledge request before changing frequency so it gets rendered if the user is on the same freq + string trns = "Tower "; + double f = globals->get_ATC_mgr()->GetFrequency(airportID, TOWER) / 100.0; + char buf[10]; + sprintf(buf, "%.2f", f); + trns += buf; + trns += " "; + trns += plane.callsign; + pending_transmission = trns; + ConditionalTransmit(30.0); + responseCounter = 0.0; + contactTower = false; + changeFreq = true; + changeFreqType = TOWER; + } + + if((contactGround) && (responseCounter >= 8.0)) { + // Acknowledge request before changing frequency so it gets rendered if the user is on the same freq + string trns = "Ground "; + double f = globals->get_ATC_mgr()->GetFrequency(airportID, GROUND) / 100.0; + char buf[10]; + sprintf(buf, "%.2f", f); + trns += buf; + trns += " "; + trns += "Good Day"; + pending_transmission = trns; + ConditionalTransmit(5.0); + responseCounter = 0.0; + contactGround = false; + changeFreq = true; + changeFreqType = GROUND; + } + + if((_taxiToGA) && (responseCounter >= 8.0)) { + // Acknowledge request before changing frequency so it gets rendered if the user is on the same freq + string trns = "GA Parking, Thank you and Good Day"; + //double f = globals->get_ATC_mgr()->GetFrequency(airportID, GROUND) / 100.0; + pending_transmission = trns; + ConditionalTransmit(5.0, 99); + _taxiToGA = false; + if(_controlled) { + tower->DeregisterAIPlane(plane.callsign); + } + // NOTE - we can't delete this instance yet since then the frequency won't get release when the message display finishes. + } + + if((_removeSelf) && (responseCounter >= 8.0)) { + _removeSelf = false; + // MEGA HACK - check if we are at a simple airport or not first instead of simply hardwiring KEMT as the only non-simple airport. + // TODO FIXME TODO FIXME !!!!!!! + if(airportID != "KEMT") globals->get_AI_mgr()->ScheduleRemoval(plane.callsign); + } + + if((changeFreq) && (responseCounter > 8.0)) { + switch(changeFreqType) { + case TOWER: + if(!tower) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR: Trying to change frequency to tower in FGAILocalTraffic, but tower is NULL!!!"); + break; + } + tuned_station = tower; + freq = (double)tower->get_freq() / 100.0; + //Transmit("DING!"); + // Contact the tower, even if only virtually + pending_transmission = plane.callsign; + pending_transmission += " at hold short for runway "; + pending_transmission += ConvertRwyNumToSpokenString(rwy.rwyID); + pending_transmission += " traffic pattern "; + if(circuitsToFly) { + pending_transmission += ConvertNumToSpokenDigits(circuitsToFly + 1); + pending_transmission += " circuits touch and go"; + } else { + pending_transmission += " one circuit to full stop"; + } + Transmit(2); + break; + case GROUND: + if(!tower) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR: Trying to change frequency to ground in FGAILocalTraffic, but tower is NULL!!!"); + break; + } + if(!ground) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR: Trying to change frequency to ground in FGAILocalTraffic, but ground is NULL!!!"); + break; + } + tower->DeregisterAIPlane(plane.callsign); + tuned_station = ground; + freq = (double)ground->get_freq() / 100.0; + break; + // And to avoid compiler warnings... + case APPROACH: break; + case ATIS: break; + case ENROUTE: break; + case DEPARTURE: break; + case INVALID: break; + } + changeFreq = false; + } + + //cout << "," << flush; + + switch(operatingState) { + case IN_PATTERN: + //cout << "In IN_PATTERN\n"; + if(!inAir) { + DoGroundElev(); + if(!elevInitGood) { + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + //cout << "TAKEOFF_ROLL, POS = " << pos.lon() << ", " << pos.lat() << ", " << pos.elev() << '\n'; + //Transform(); + _aip.setVisible(true); + //cout << "Making plane visible!\n"; + elevInitGood = true; + } + } + } + FlyTrafficPattern(dt); + Transform(); + break; + case TAXIING: + //cout << "In TAXIING\n"; + //cout << "*" << flush; + if(!elevInitGood) { + //DoGroundElev(); + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + //Transform(); + _aip.setVisible(true); + //Transform(); + //cout << "Making plane visible!\n"; + elevInitGood = true; + } + } + DoGroundElev(); + //cout << "~" << flush; + if(!((holdingShort) && (!clearedToLineUp))) { + //cout << "|" << flush; + Taxi(dt); + } + //cout << ";" << flush; + if((clearedToTakeOff) && (responseCounter >= 8.0)) { + // possible assumption that we're at the hold short here - may not always hold + // TODO - sort out the case where we're cleared to line-up first and then cleared to take-off on the rwy. + taxiState = TD_LINING_UP; + //cout << "A" << endl; + path = ground->GetPath(holdShortNode, rwy.rwyID); + //cout << "B" << endl; + if(!path.size()) { // Assume no facility file so we'll just taxi to a point on the runway near the threshold + //cout << "C" << endl; + node* np = new node; + np->struct_type = NODE; + np->pos = ortho.ConvertFromLocal(Point3D(0.0, 10.0, 0.0)); + path.push_back(np); + } else { + //cout << "D" << endl; + } + /* + cout << "path returned was:" << endl; + for(unsigned int i=0; i<path.size(); ++i) { + switch(path[i]->struct_type) { + case NODE: + cout << "NODE " << ((node*)(path[i]))->nodeID << endl; + break; + case ARC: + cout << "ARC\n"; + break; + } + } + */ + clearedToTakeOff = false; // We *are* still cleared - this simply stops the response recurring!! + holdingShort = false; + string trns = "Cleared for take-off "; + trns += plane.callsign; + pending_transmission = trns; + Transmit(); + StartTaxi(); + } + //cout << "^" << flush; + Transform(); + break; + case PARKED: + //cout << "In PARKED\n"; + if(!elevInitGood) { + DoGroundElev(); + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + //Transform(); + _aip.setVisible(true); + //Transform(); + //cout << "Making plane visible!\n"; + elevInitGood = true; + } + } + + if(circuitsToFly) { + if((taxiRequestPending) && (taxiRequestCleared)) { + //cout << "&" << flush; + // Get the active runway details (in case they've changed since init) + GetRwyDetails(airportID); + + // Get the takeoff node for the active runway, get a path to it and start taxiing + path = ground->GetPathToHoldShort(ourGate, rwy.rwyID); + if(path.size() < 2) { + // something has gone wrong + SG_LOG(SG_ATC, SG_ALERT, "Invalid path from gate to theshold in FGAILocalTraffic::FlyCircuits\n"); + return; + } + /* + cout << "path returned was:\n"; + for(unsigned int i=0; i<path.size(); ++i) { + switch(path[i]->struct_type) { + case NODE: + cout << "NODE " << ((node*)(path[i]))->nodeID << endl; + break; + case ARC: + cout << "ARC\n"; + break; + } + } + */ + path.erase(path.begin()); // pop the gate - we're here already! + taxiState = TD_OUTBOUND; + taxiRequestPending = false; + holdShortNode = (node*)(*(path.begin() + path.size())); + StartTaxi(); + } else if(!taxiRequestPending) { + //cout << "(" << flush; + // Do some communication + // airport name + tower + airplane callsign + location + request taxi for + operation type + ? + string trns = ""; + if(_controlled) { + trns += tower->get_name(); + trns += " tower "; + } else { + trns += "Traffic "; + // TODO - get the airport name somehow if uncontrolled + } + trns += plane.callsign; + trns += " on apron parking request taxi for traffic pattern"; + //cout << "trns = " << trns << endl; + pending_transmission = trns; + Transmit(1); + taxiRequestCleared = false; + taxiRequestPending = true; + } + } + + //cout << "!" << flush; + + // Maybe the below should be set when we get to the threshold and prepare for TO? + // FIXME TODO - pattern direction is still hardwired + patternDirection = -1; // Left + // At the bare minimum we ought to make sure it goes the right way at dual parallel rwy airports! + if(rwy.rwyID.size() == 3) { + patternDirection = (rwy.rwyID.substr(2,1) == "R" ? 1 : -1); + } + // Do nothing + Transform(); + //cout << ")" << flush; + break; + default: + break; + } + //cout << "I " << flush; + + //cout << "Update _pos = " << _pos << ", vis = " << _aip.getVisible() << '\n'; + + // Convienience output for AI debugging using the property logger + //fgSetDouble("/AI/Local1/ortho-x", (ortho.ConvertToLocal(_pos)).x()); + //fgSetDouble("/AI/Local1/ortho-y", (ortho.ConvertToLocal(_pos)).y()); + //fgSetDouble("/AI/Local1/elev", _pos.elev() * SG_METER_TO_FEET); + + // And finally, call parent. + FGAIPlane::Update(dt); +} + +void FGAILocalTraffic::RegisterTransmission(int code) { + switch(code) { + case 1: // taxi request cleared + taxiRequestCleared = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to taxi..."); + break; + case 2: // contact tower + responseCounter = 0; + contactTower = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " told to contact tower..."); + break; + case 3: // Cleared to line up + responseCounter = 0; + clearedToLineUp = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to line-up..."); + break; + case 4: // cleared to take-off + responseCounter = 0; + clearedToTakeOff = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to take-off..."); + break; + case 5: // contact ground + responseCounter = 0; + contactGround = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " told to contact ground..."); + break; + // case 6 is a temporary mega-hack for controlled airports without separate ground control + case 6: // taxi to the GA parking + responseCounter = 0; + _taxiToGA = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " told to taxi to the GA parking..."); + break; + case 7: // Cleared to land (also implies cleared for the option + _clearedToLand = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " cleared to land..."); + break; + case 13: // Go around! + responseCounter = 0; + goAround = true; + _clearedToLand = false; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " told to go-around!!"); + break; + default: + break; + } +} + +// Fly a traffic pattern +// FIXME - far too much of the mechanics of turning, rolling, accellerating, descending etc is in here. +// Move it out to FGAIPlane and have FlyTrafficPattern just specify what to do, not the implementation. +void FGAILocalTraffic::FlyTrafficPattern(double dt) { + // Need to differentiate between in-air (IAS governed) and on-ground (vel governed) + // Take-off is an interesting case - we are on the ground but takeoff speed is IAS governed. + + // WIND + // Wind has two effects - a mechanical one in that IAS translates to a different vel, and the hdg != track, + // but also a piloting effect, in that the AI must be able to descend at a different rate in order to hit the threshold. + + //cout << "dt = " << dt << '\n'; + double dist = 0; + // ack - I can't remember how long a rate 1 turn is meant to take. + double turn_time = 60.0; // seconds - TODO - check this guess + double turn_circumference; + double turn_radius; + Point3D orthopos = ortho.ConvertToLocal(_pos); // ortho position of the plane + //cout << "runway elev = " << rwy.threshold_pos.elev() << ' ' << rwy.threshold_pos.elev() * SG_METER_TO_FEET << '\n'; + //cout << "elev = " << _pos.elev() << ' ' << _pos.elev() * SG_METER_TO_FEET << '\n'; + + // HACK FOR TESTING - REMOVE + //cout << "Calling ExitRunway..." << endl; + //ExitRunway(orthopos); + //return; + // END HACK + + //wind + double wind_from = wind_from_hdg->getDoubleValue(); + double wind_speed = wind_speed_knots->getDoubleValue(); + + double dveldt; + + switch(leg) { + case TAKEOFF_ROLL: + //inAir = false; + track = rwy.hdg; + if(vel < 80.0) { + double dveldt = 5.0; + vel += dveldt * dt; + } + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + } + IAS = vel + (cos((_hdg - wind_from) * DCL_DEGREES_TO_RADIANS) * wind_speed); + if(IAS >= 70) { + leg = CLIMBOUT; + SetTrack(rwy.hdg); // Hands over control of turning to AIPlane + _pitch = 10.0; + IAS = best_rate_of_climb_speed; + //slope = 7.0; + slope = 6.0; // Reduced it slightly since it's climbing a lot steeper than I can in the JSBSim C172. + inAir = true; + } + break; + case CLIMBOUT: + // Turn to crosswind if above 700ft AND if other traffic allows + // (decided in FGTower and accessed through GetCrosswindConstraint(...)). + // According to AIM, traffic should climb to within 300ft of pattern altitude before commencing crosswind turn. + // TODO - At hot 'n high airports this may be 500ft AGL though - need to make this a variable. + if((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 700) { + double cc = 0.0; + if(tower->GetCrosswindConstraint(cc)) { + if(orthopos.y() > cc) { + //cout << "Turning to crosswind, distance from threshold = " << orthopos.y() << '\n'; + leg = TURN1; + } + } else if(orthopos.y() > 1500.0) { // Added this constraint as a hack to prevent turning too early when going around. + // TODO - We should be doing it as a distance from takeoff end, not theshold end though. + //cout << "Turning to crosswind, distance from threshold = " << orthopos.y() << '\n'; + leg = TURN1; + } + } + // Need to check for levelling off in case we can't turn crosswind as soon + // as we would like due to other traffic. + if((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) { + slope = 0.0; + _pitch = 0.0; + IAS = 80.0; // FIXME - use smooth transistion to new speed and attitude. + } + if(goAround && !goAroundCalled) { + if(responseCounter > 5.5) { + pending_transmission = plane.callsign; + pending_transmission += " going around"; + Transmit(); + goAroundCalled = true; + } + } + break; + case TURN1: + SetTrack(rwy.hdg + (90.0 * patternDirection)); + if((track < (rwy.hdg - 89.0)) || (track > (rwy.hdg + 89.0))) { + leg = CROSSWIND; + } + break; + case CROSSWIND: + goAround = false; + if((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) { + slope = 0.0; + _pitch = 0.0; + IAS = 80.0; // FIXME - use smooth transistion to new speed + } + // turn 1000m out for now, taking other traffic into accout + if(fabs(orthopos.x()) > 900) { + double dd = 0.0; + if(tower->GetDownwindConstraint(dd)) { + if(fabs(orthopos.x()) > fabs(dd)) { + //cout << "Turning to downwind, distance from centerline = " << fabs(orthopos.x()) << '\n'; + leg = TURN2; + } + } else { + //cout << "Turning to downwind, distance from centerline = " << fabs(orthopos.x()) << '\n'; + leg = TURN2; + } + } + break; + case TURN2: + SetTrack(rwy.hdg - (180 * patternDirection)); + // just in case we didn't make height on crosswind + if((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 1000) { + slope = 0.0; + _pitch = 0.0; + IAS = 80.0; // FIXME - use smooth transistion to new speed + } + if((track < (rwy.hdg - 179.0)) || (track > (rwy.hdg + 179.0))) { + leg = DOWNWIND; + transmitted = false; + } + break; + case DOWNWIND: + // just in case we didn't make height on crosswind + if(((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET > 995) && ((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET < 1015)) { + slope = 0.0; + _pitch = 0.0; + IAS = 90.0; // FIXME - use smooth transistion to new speed + } + if((_pos.elev() - rwy.threshold_pos.elev()) * SG_METER_TO_FEET >= 1015) { + slope = -1.0; + _pitch = -1.0; + IAS = 90.0; // FIXME - use smooth transistion to new speed + } + if((orthopos.y() < 0) && (!transmitted)) { + TransmitPatternPositionReport(); + transmitted = true; + } + if((orthopos.y() < -100) && (!descending)) { + //cout << "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDdddd\n"; + // Maybe we should think about when to start descending. + // For now we're assuming that we aim to follow the same glidepath regardless of wind. + double d1; + double d2; + CalculateSoD((tower->GetBaseConstraint(d1) ? d1 : -1000.0), (tower->GetDownwindConstraint(d2) ? d2 : 1000.0 * patternDirection), (patternDirection ? true : false)); + if(SoD.leg == DOWNWIND) { + descending = (orthopos.y() < SoD.y ? true : false); + } + + } + if(descending) { + slope = -5.5; // FIXME - calculate to descent at 500fpm and hit the desired point on the runway (taking wind into account as well!!) + _pitch = -3.0; + IAS = 85.0; + } + + // Try and arrange to turn nicely onto base + turn_circumference = IAS * 0.514444 * turn_time; + //Hmmm - this is an interesting one - ground vs airspeed in relation to turn radius + //We'll leave it as a hack with IAS for now but it needs revisiting. + turn_radius = turn_circumference / (2.0 * DCL_PI); + if(orthopos.y() < -1000.0 + turn_radius) { + //if(orthopos.y() < -980) { + double bb = 0.0; + if(tower->GetBaseConstraint(bb)) { + if(fabs(orthopos.y()) > fabs(bb)) { + //cout << "Turning to base, distance from threshold = " << fabs(orthopos.y()) << '\n'; + leg = TURN3; + transmitted = false; + IAS = 80.0; + } + } else { + //cout << "Turning to base, distance from threshold = " << fabs(orthopos.y()) << '\n'; + leg = TURN3; + transmitted = false; + IAS = 80.0; + } + } + break; + case TURN3: + SetTrack(rwy.hdg - (90 * patternDirection)); + if(fabs(rwy.hdg - track) < 91.0) { + leg = BASE; + } + break; + case BASE: + if(!transmitted) { + // Base report should only be transmitted at uncontrolled airport - not towered. + if(!_controlled) TransmitPatternPositionReport(); + transmitted = true; + } + + if(!descending) { + double d1; + // Make downwind leg position artifically large to avoid any chance of SoD being returned as + // on downwind when we are already on base. + CalculateSoD((tower->GetBaseConstraint(d1) ? d1 : -1000.0), (10000.0 * patternDirection), (patternDirection ? true : false)); + if(SoD.leg == BASE) { + descending = (fabs(orthopos.y()) < fabs(SoD.y) ? true : false); + } + + } + if(descending) { + slope = -5.5; // FIXME - calculate to descent at 500fpm and hit the threshold (taking wind into account as well!!) + _pitch = -4.0; + IAS = 70.0; + } + + // Try and arrange to turn nicely onto final + turn_circumference = IAS * 0.514444 * turn_time; + //Hmmm - this is an interesting one - ground vs airspeed in relation to turn radius + //We'll leave it as a hack with IAS for now but it needs revisiting. + turn_radius = turn_circumference / (2.0 * DCL_PI); + if(fabs(orthopos.x()) < (turn_radius + 50)) { + leg = TURN4; + transmitted = false; + //roll = -20; + } + break; + case TURN4: + SetTrack(rwy.hdg); + if(fabs(track - rwy.hdg) < 0.6) { + leg = FINAL; + vel = nominal_final_speed; + } + break; + case FINAL: + if(goAround && responseCounter > 2.0) { + leg = CLIMBOUT; + _pitch = 8.0; + IAS = best_rate_of_climb_speed; + slope = 5.0; // A bit less steep than the initial climbout. + inAir = true; + goAroundCalled = false; + descending = false; + break; + } + LevelWings(); + if(!transmitted) { + if((!_controlled) || (!_clearedToLand)) TransmitPatternPositionReport(); + transmitted = true; + } + if(!descending) { + // Make base leg position artifically large to avoid any chance of SoD being returned as + // on base or downwind when we are already on final. + CalculateSoD(-10000.0, (1000.0 * patternDirection), (patternDirection ? true : false)); + if(SoD.leg == FINAL) { + descending = (fabs(orthopos.y()) < fabs(SoD.y) ? true : false); + } + + } + if(descending) { + if(orthopos.y() < -50.0) { + double thesh_offset = 30.0; + slope = atan((_pos.elev() - fgGetAirportElev(airportID)) / (orthopos.y() - thesh_offset)) * DCL_RADIANS_TO_DEGREES; + //cout << "slope = " << slope << ", elev = " << _pos.elev() << ", apt_elev = " << fgGetAirportElev(airportID) << ", op.y = " << orthopos.y() << '\n'; + if(slope < -10.0) slope = -10.0; + _savedSlope = slope; + _pitch = -4.0; + IAS = 70.0; + } else { + if(_pos.elev() < (rwy.threshold_pos.elev()+10.0+wheelOffset)) { + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + if(_pos.elev() < (_aip.getSGLocation()->get_cur_elev_m() + wheelOffset + 1.0)) { + slope = -2.0; + _pitch = 1.0; + IAS = 55.0; + } else if(_pos.elev() < (_aip.getSGLocation()->get_cur_elev_m() + wheelOffset + 5.0)) { + slope = -4.0; + _pitch = -2.0; + IAS = 60.0; + } else { + slope = _savedSlope; + _pitch = -3.0; + IAS = 65.0; + } + } else { + // Elev not determined + slope = _savedSlope; + _pitch = -3.0; + IAS = 65.0; + } + } else { + slope = _savedSlope; + _pitch = -3.0; + IAS = 65.0; + } + } + } + // Try and track the extended centreline + SetTrack(rwy.hdg - (0.2 * orthopos.x())); + //cout << "orthopos.x() = " << orthopos.x() << " hdg = " << hdg << '\n'; + if(_pos.elev() < (rwy.threshold_pos.elev()+20.0+wheelOffset)) { + DoGroundElev(); // Need to call it here expicitly on final since it's only called + // for us in update(...) when the inAir flag is false. + } + if(_pos.elev() < (rwy.threshold_pos.elev()+10.0+wheelOffset)) { + //slope = -1.0; + //_pitch = 1.0; + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + if((_aip.getSGLocation()->get_cur_elev_m() + wheelOffset) > _pos.elev()) { + slope = 0.0; + _pitch = 0.0; + leg = LANDING_ROLL; + inAir = false; + LevelWings(); + ClearTrack(); // Take over explicit track handling since AIPlane currently always banks when changing course + } + } // else need a fallback position based on arpt elev in case ground elev determination fails? + } else { + // TODO + } + break; + case LANDING_ROLL: + //inAir = false; + descending = false; + if(_aip.getSGLocation()->get_cur_elev_m() > -9990.0) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + } + track = rwy.hdg; + dveldt = -5.0; + vel += dveldt * dt; + // FIXME - differentiate between touch and go and full stops + if(vel <= 15.0) { + //cout << "Vel <= 15.0, circuitsToFly = " << circuitsToFly << endl; + if(circuitsToFly <= 0) { + //cout << "Calling ExitRunway..." << endl; + ExitRunway(orthopos); + return; + } else { + //cout << "Taking off again..." << endl; + leg = TAKEOFF_ROLL; + --circuitsToFly; + } + } + break; + case LEG_UNKNOWN: + break; + } + + if(inAir) { + // FIXME - at the moment this is a bit screwy + // The velocity correction is applied based on the relative headings. + // Then the heading is changed based on the velocity. + // Which comes first, the chicken or the egg? + // Does it really matter? + + // Apply wind to ground-relative velocity if in the air + vel = IAS - (cos((_hdg - wind_from) * DCL_DEGREES_TO_RADIANS) * wind_speed); + //crab = f(track, wind, vel); + // The vector we need to fly is our desired vector minus the wind vector + // TODO - we probably ought to use plib's built in vector types and operations for this + // ie. There's almost *certainly* a better way to do this! + double gxx = vel * sin(track * DCL_DEGREES_TO_RADIANS); // Plane desired velocity x component wrt ground + double gyy = vel * cos(track * DCL_DEGREES_TO_RADIANS); // Plane desired velocity y component wrt ground + double wxx = wind_speed * sin((wind_from + 180.0) * DCL_DEGREES_TO_RADIANS); // Wind velocity x component + double wyy = wind_speed * cos((wind_from + 180.0) * DCL_DEGREES_TO_RADIANS); // Wind velocity y component + double axx = gxx - wxx; // Plane in-air velocity x component + double ayy = gyy - wyy; // Plane in-air velocity y component + // Now we want the angle between gxx and axx (which is the crab) + double maga = sqrt(axx*axx + ayy*ayy); + double magg = sqrt(gxx*gxx + gyy*gyy); + crab = acos((axx*gxx + ayy*gyy) / (maga * magg)); + // At this point this works except we're getting the modulus of the angle + //cout << "crab = " << crab << '\n'; + + // Make sure both headings are in the 0->360 circle in order to get sane differences + dclBoundHeading(wind_from); + dclBoundHeading(track); + if(track > wind_from) { + if((track - wind_from) <= 180) { + crab *= -1.0; + } + } else { + if((wind_from - track) >= 180) { + crab *= -1.0; + } + } + } else { // on the ground - crab dosen't apply + crab = 0.0; + } + + //cout << "X " << orthopos.x() << " Y " << orthopos.y() << " SLOPE " << slope << " elev " << _pos.elev() * SG_METER_TO_FEET << '\n'; + + _hdg = track + crab; + dist = vel * 0.514444 * dt; + _pos = dclUpdatePosition(_pos, track, slope, dist); +} + +// Pattern direction is true for right, false for left +void FGAILocalTraffic::CalculateSoD(double base_leg_pos, double downwind_leg_pos, bool pattern_direction) { + // For now we'll ignore wind and hardwire the glide angle. + double ga = 5.5; //degrees + double pa = 1000.0 * SG_FEET_TO_METER; // pattern altitude in meters + // FIXME - get glideslope angle and pattern altitude agl from airport details if available + + // For convienience, we'll have +ve versions of the input distances + double blp = fabs(base_leg_pos); + double dlp = fabs(downwind_leg_pos); + + //double turn_allowance = 150.0; // Approximate distance in meters that a 90deg corner is shortened by turned in a light plane. + + double stod = pa / tan(ga * DCL_DEGREES_TO_RADIANS); // distance in meters from touchdown point to start descent + //cout << "Descent to start = " << stod << " meters out\n"; + if(stod < blp) { // Start descending on final + SoD.leg = FINAL; + SoD.y = stod * -1.0; + SoD.x = 0.0; + } else if(stod < (blp + dlp)) { // Start descending on base leg + SoD.leg = BASE; + SoD.y = blp * -1.0; + SoD.x = (pattern_direction ? (stod - dlp) : (stod - dlp) * -1.0); + } else { // Start descending on downwind leg + SoD.leg = DOWNWIND; + SoD.x = (pattern_direction ? dlp : dlp * -1.0); + SoD.y = (blp - (stod - (blp + dlp))) * -1.0; + } +} + +void FGAILocalTraffic::TransmitPatternPositionReport(void) { + // airport name + "traffic" + airplane callsign + pattern direction + pattern leg + rwy + ? + string trns = ""; + int code = 0; + + trns += tower->get_name(); + trns += " Traffic "; + trns += plane.callsign; + if(patternDirection == 1) { + trns += " right "; + } else { + trns += " left "; + } + + // We could probably get rid of this whole switch statement and just pass a string containing the leg from the FlyPattern function. + switch(leg) { // We'll assume that transmissions in turns are intended for next leg - do pilots ever call out that they are in the turn? + case TURN1: + // Fall through to CROSSWIND + case CROSSWIND: // I don't think this case will be used here but it can't hurt to leave it in + trns += "crosswind "; + break; + case TURN2: + // Fall through to DOWNWIND + case DOWNWIND: + trns += "downwind "; + code = 11; + break; + case TURN3: + // Fall through to BASE + case BASE: + trns += "base "; + break; + case TURN4: + // Fall through to FINAL + case FINAL: // maybe this should include long/short final if appropriate? + trns += "final "; + code = 13; + break; + default: // Hopefully this won't be used + trns += "pattern "; + break; + } + trns += ConvertRwyNumToSpokenString(rwy.rwyID); + + trns += " "; + + // And add the airport name again + trns += tower->get_name(); + + pending_transmission = trns; + ConditionalTransmit(60.0, code); // Assume a report of this leg will be invalid if we can't transmit within a minute. +} + +// Callback handler +// TODO - Really should enumerate these coded values. +void FGAILocalTraffic::ProcessCallback(int code) { + // 1 - Request Departure from ground + // 2 - Report at hold short + // 3 - Report runway vacated + // 10 - report crosswind + // 11 - report downwind + // 12 - report base + // 13 - report final + if(code == 1) { + ground->RequestDeparture(plane, this); + } else if(code == 2) { + tower->ContactAtHoldShort(plane, this, CIRCUIT); + } else if(code == 3) { + tower->ReportRunwayVacated(plane.callsign); + } else if(code == 11) { + tower->ReportDownwind(plane.callsign); + } else if(code == 13) { + tower->ReportFinal(plane.callsign); + } else if(code == 99) { // Flag this instance for deletion + responseCounter = 0; + _removeSelf = true; + SG_LOG(SG_ATC, SG_INFO, "AI local traffic " << plane.callsign << " delete instance callback called."); + } +} + +void FGAILocalTraffic::ExitRunway(const Point3D& orthopos) { + //cout << "In ExitRunway" << endl; + //cout << "Runway ID is " << rwy.ID << endl; + + _clearedToLand = false; + + node_array_type exitNodes = ground->GetExits(rwy.rwyID); //I suppose we ought to have some fallback for rwy with no defined exits? + /* + cout << "Node ID's of exits are "; + for(unsigned int i=0; i<exitNodes.size(); ++i) { + cout << exitNodes[i]->nodeID << ' '; + } + cout << endl; + */ + if(exitNodes.size()) { + //Find the next exit from orthopos.y + double d; + double dist = 100000; //ie. longer than any runway in existance + double backdist = 100000; + node_array_iterator nItr = exitNodes.begin(); + node* rwyExit = *(exitNodes.begin()); + //int gateID; //This might want to be more persistant at some point + while(nItr != exitNodes.end()) { + d = ortho.ConvertToLocal((*nItr)->pos).y() - ortho.ConvertToLocal(_pos).y(); //FIXME - consider making orthopos a class variable + if(d > 0.0) { + if(d < dist) { + dist = d; + rwyExit = *nItr; + } + } else { + if(fabs(d) < backdist) { + backdist = d; + //TODO - need some logic here that if we don't get a forward exit we turn round and store the backwards one + } + } + ++nItr; + } + ourGate = ground->GetGateNode(); + if(ourGate == NULL) { + // Implies no available gates - what shall we do? + // For now just vanish the plane - possibly we can make this more elegant in the future + SG_LOG(SG_ATC, SG_ALERT, "No gate found by FGAILocalTraffic whilst landing at " << airportID << '\n'); + //_aip.setVisible(false); + //cout << "Setting visible false\n"; + operatingState = PARKED; + return; + } + path = ground->GetPath(rwyExit, ourGate); + /* + cout << "path returned was:" << endl; + for(unsigned int i=0; i<path.size(); ++i) { + switch(path[i]->struct_type) { + case NODE: + cout << "NODE " << ((node*)(path[i]))->nodeID << endl; + break; + case ARC: + cout << "ARC\n"; + break; + } + } + */ + taxiState = TD_INBOUND; + StartTaxi(); + } else { + // Something must have gone wrong with the ground network file - or there is only a rwy here and no exits defined + SG_LOG(SG_ATC, SG_INFO, "No exits found by FGAILocalTraffic from runway " << rwy.rwyID << " at " << airportID << '\n'); + //if(airportID == "KRHV") cout << "No exits found by " << plane.callsign << " from runway " << rwy.rwyID << " at " << airportID << '\n'; + // What shall we do - just remove the plane from sight? + _aip.setVisible(false); + _invisible = true; + //cout << "Setting visible false\n"; + //tower->ReportRunwayVacated(plane.callsign); + string trns = "Clear of the runway "; + trns += plane.callsign; + pending_transmission = trns; + Transmit(3); + operatingState = PARKED; + } +} + +// Set the class variable nextTaxiNode to the next node in the path +// and update taxiPathPos, the class variable path iterator position +// TODO - maybe should return error codes to the calling function if we fail here +void FGAILocalTraffic::GetNextTaxiNode() { + //cout << "GetNextTaxiNode called " << endl; + //cout << "taxiPathPos = " << taxiPathPos << endl; + ground_network_path_iterator pathItr = path.begin() + taxiPathPos; + if(pathItr == path.end()) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - no more nodes in path\n"); + } else { + if((*pathItr)->struct_type == NODE) { + //cout << "ITS A NODE" << endl; + //*pathItr = new node; + nextTaxiNode = (node*)*pathItr; + ++taxiPathPos; + //delete pathItr; + } else { + //cout << "ITS NOT A NODE" << endl; + //The first item in found must have been an arc + //Assume for now that it was straight + pathItr++; + taxiPathPos++; + if(pathItr == path.end()) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - path ended with an arc\n"); + } else if((*pathItr)->struct_type == NODE) { + nextTaxiNode = (node*)*pathItr; + ++taxiPathPos; + } else { + //OOPS - two non-nodes in a row - that shouldn't happen ATM + SG_LOG(SG_ATC, SG_ALERT, "ERROR IN AILocalTraffic::GetNextTaxiNode - two non-nodes in sequence\n"); + } + } + } +} + +// StartTaxi - set up the taxiing state - call only at the start of taxiing +void FGAILocalTraffic::StartTaxi() { + //cout << "StartTaxi called" << endl; + operatingState = TAXIING; + + taxiPathPos = 0; + + //Set the desired heading + //Assume we are aiming for first node on path + //Eventually we may need to consider the fact that we might start on a curved arc and + //not be able to head directly for the first node. + GetNextTaxiNode(); // sets the class variable nextTaxiNode to the next taxi node! + desiredTaxiHeading = GetHeadingFromTo(_pos, nextTaxiNode->pos); + //cout << "First taxi heading is " << desiredTaxiHeading << endl; +} + +// speed in knots, headings in degrees, radius in meters. +static double TaxiTurnTowardsHeading(double current_hdg, double desired_hdg, double speed, double radius, double dt) { + // wrap heading - this prevents a logic bug where the plane would just go round in circles!! + while(current_hdg < 0.0) { + current_hdg += 360.0; + } + while(current_hdg > 360.0) { + current_hdg -= 360.0; + } + if(fabs(current_hdg - desired_hdg) > 0.1) { + // Which is the quickest direction to turn onto heading? + if(desired_hdg > current_hdg) { + if((desired_hdg - current_hdg) <= 180) { + // turn right + current_hdg += ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0; + // TODO - check that increments are less than the delta that we check for the right direction + // Probably need to reduce convergence speed as convergence is reached + } else { + current_hdg -= ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0; + } + } else { + if((current_hdg - desired_hdg) <= 180) { + // turn left + current_hdg -= ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0; + // TODO - check that increments are less than the delta that we check for the right direction + // Probably need to reduce convergence speed as convergence is reached + } else { + current_hdg += ((speed * 0.514444 * dt) / (radius * DCL_PI)) * 180.0; + } + } + } + return(current_hdg); +} + +void FGAILocalTraffic::Taxi(double dt) { + //cout << "Taxi called" << endl; + // Logic - if we are further away from next point than turn radius then head for it + // If we have reached turning point then get next point and turn onto that heading + // Look out for the finish!! + + //Point3D orthopos = ortho.ConvertToLocal(pos); // ortho position of the plane + desiredTaxiHeading = GetHeadingFromTo(_pos, nextTaxiNode->pos); + + bool lastNode = (taxiPathPos == path.size() ? true : false); + if(lastNode) { + //cout << "LAST NODE\n"; + } + + // HACK ALERT! - for now we will taxi at constant speed for straights and turns + + // Remember that hdg is always equal to track when taxiing so we don't have to consider them both + double dist_to_go = dclGetHorizontalSeparation(_pos, nextTaxiNode->pos); // we may be able to do this more cheaply using orthopos + //cout << "dist_to_go = " << dist_to_go << endl; + if((nextTaxiNode->type == GATE) && (dist_to_go <= 0.1)) { + // This might be more robust to outward paths starting with a gate if we check for either + // last node or TD_INBOUND ? + // park up + operatingState = PARKED; + } else if(((dist_to_go > taxiTurnRadius) || (nextTaxiNode->type == GATE)) && (!liningUp)){ + // if the turn radius is r, and speed is s, then in a time dt we turn through + // ((s.dt)/(PI.r)) x 180 degrees + // or alternatively (s.dt)/r radians + //cout << "hdg = " << hdg << " desired taxi heading = " << desiredTaxiHeading << '\n'; + _hdg = TaxiTurnTowardsHeading(_hdg, desiredTaxiHeading, nominalTaxiSpeed, taxiTurnRadius, dt); + double vel = nominalTaxiSpeed; + //cout << "vel = " << vel << endl; + double dist = vel * 0.514444 * dt; + //cout << "dist = " << dist << endl; + double track = _hdg; + //cout << "track = " << track << endl; + double slope = 0.0; + _pos = dclUpdatePosition(_pos, track, slope, dist); + //cout << "Updated position...\n"; + if(_aip.getSGLocation()->get_cur_elev_m() > -9990) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + } // else don't change the elev until we get a valid ground elev again! + } else if(lastNode) { + if(taxiState == TD_LINING_UP) { + if((!liningUp) && (dist_to_go <= taxiTurnRadius)) { + liningUp = true; + } + if(liningUp) { + _hdg = TaxiTurnTowardsHeading(_hdg, rwy.hdg, nominalTaxiSpeed, taxiTurnRadius, dt); + double vel = nominalTaxiSpeed; + //cout << "vel = " << vel << endl; + double dist = vel * 0.514444 * dt; + //cout << "dist = " << dist << endl; + double track = _hdg; + //cout << "track = " << track << endl; + double slope = 0.0; + _pos = dclUpdatePosition(_pos, track, slope, dist); + //cout << "Updated position...\n"; + if(_aip.getSGLocation()->get_cur_elev_m() > -9990) { + _pos.setelev(_aip.getSGLocation()->get_cur_elev_m() + wheelOffset); + } // else don't change the elev until we get a valid ground elev again! + if(fabs(_hdg - rwy.hdg) <= 1.0) { + operatingState = IN_PATTERN; + leg = TAKEOFF_ROLL; + inAir = false; + liningUp = false; + } + } + } else if(taxiState == TD_OUTBOUND) { + // Pause awaiting further instructions + // and for now assume we've reached the hold-short node + holdingShort = true; + } // else at the moment assume TD_INBOUND always ends in a gate in which case we can ignore it + } else { + // Time to turn (we've already checked it's not the end we're heading for). + // set the target node to be the next node which will prompt automatically turning onto + // the right heading in the stuff above, with the usual provisos applied. + GetNextTaxiNode(); + // For now why not just recursively call this function? + Taxi(dt); + } +} + + +// Warning - ground elev determination is CPU intensive +// Either this function or the logic of how often it is called +// will almost certainly change. +void FGAILocalTraffic::DoGroundElev() { + // It would be nice if we could set the correct tile center here in order to get a correct + // answer with one call to the function, but what I tried in the two commented-out lines + // below only intermittently worked, and I haven't quite groked why yet. + //SGBucket buck(pos.lon(), pos.lat()); + //aip.getSGLocation()->set_tile_center(Point3D(buck.get_center_lon(), buck.get_center_lat(), 0.0)); + + // Only do the proper hitlist stuff if we are within visible range of the viewer. + double visibility_meters = fgGetDouble("/environment/visibility-m"); + FGViewer* vw = globals->get_current_view(); + if(dclGetHorizontalSeparation(_pos, Point3D(vw->getLongitude_deg(), vw->getLatitude_deg(), 0.0)) > visibility_meters) { + _aip.getSGLocation()->set_cur_elev_m(aptElev); + return; + } + + // FIXME: make shure the pos.lat/pos.lon values are in degrees ... + double range = 500.0; + double lat = _aip.getSGLocation()->getLatitude_deg(); + double lon = _aip.getSGLocation()->getLongitude_deg(); + if (!globals->get_tile_mgr()->scenery_available(lat, lon, range)) { + // Try to shedule tiles for that position. + globals->get_tile_mgr()->update( _aip.getSGLocation(), range ); + } + + // FIXME: make shure the pos.lat/pos.lon values are in degrees ... + double alt; + if (globals->get_scenery()->get_elevation_m(lat, lon, 20000.0, alt, 0)) + _aip.getSGLocation()->set_cur_elev_m(alt); +} + diff --git a/src/ATCDCL/AILocalTraffic.hxx b/src/ATCDCL/AILocalTraffic.hxx new file mode 100644 index 000000000..a4e1505f6 --- /dev/null +++ b/src/ATCDCL/AILocalTraffic.hxx @@ -0,0 +1,233 @@ +// FGAILocalTraffic - AIEntity derived class with enough logic to +// fly and interact with the traffic pattern. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_AILocalTraffic_HXX +#define _FG_AILocalTraffic_HXX + +#include <simgear/math/point3d.hxx> +#include <Main/fg_props.hxx> + +#include "AIPlane.hxx" +#include "ATCProjection.hxx" +#include "ground.hxx" + +class FGGround; +class FGTower; +struct Gate; + +#include <string> +SG_USING_STD(string); + +enum TaxiState { + TD_INBOUND, + TD_OUTBOUND, + TD_NONE, + TD_LINING_UP +}; + +enum OperatingState { + IN_PATTERN, + TAXIING, + PARKED, + EN_ROUTE +}; + +struct StartOfDescent { + PatternLeg leg; + double x; // Runway aligned orthopos + double y; // ditto +}; + +class FGAILocalTraffic : public FGAIPlane { + +public: + + // At the moment we expect the expanded short form callsign - eventually we will just want the reg + type. + FGAILocalTraffic(); + ~FGAILocalTraffic(); + + // Initialise + bool Init(const string& callsign, const string& ICAO, OperatingState initialState = PARKED, PatternLeg initialLeg = DOWNWIND); + + // Run the internal calculations + void Update(double dt); + + // Go out and practice circuits + void FlyCircuits(int numCircuits, bool tag); + + // Return what type of landing we're doing on this circuit + LandingType GetLandingOption(); + + // TODO - this will get more complex and moved into the main class + // body eventually since the position approved to taxi to will have + // to be passed. + inline void ApproveTaxiRequest() {taxiRequestCleared = true;} + + inline void DenyTaxiRequest() {taxiRequestCleared = false;} + + void RegisterTransmission(int code); + + // Process callbacks sent by base class + // (These codes are not related to the codes above) + void ProcessCallback(int code); + + // This is a hack and will probably go eventually + inline bool AtHoldShort() {return holdingShort;} + +protected: + + // Attempt to enter the traffic pattern in a reasonably intelligent manner + void EnterTrafficPattern(double dt); + + // Set up the internal state to be consistent for a downwind entry. + void DownwindEntry(); + + // Ditto for straight-in + void StraightInEntry(bool des = false); + + // Do what is necessary to land and parkup at home airport + void ReturnToBase(double dt); + + // Airport/runway/pattern details + string airportID; // The ICAO code of the airport that we're operating around + double aptElev; // Airport elevation + FGGround* ground; // A pointer to the ground control. + FGTower* tower; // A pointer to the tower control. + bool _controlled; // Set true if we find tower control working for the airport, false otherwise. + RunwayDetails rwy; + double patternDirection; // 1 for right, -1 for left (This is double because we multiply/divide turn rates + // with it to get RH/LH turns - DON'T convert it to int under ANY circumstances!! + double glideAngle; // Assumed to be visual glidepath angle for FGAILocalTraffic - can be found at www.airnav.com + // Its conceivable that patternDirection and glidePath could be moved into the RunwayDetails structure. + + // Its possible that this might be moved out to the ground/airport class at some point. + FGATCAlignedProjection ortho; // Orthogonal mapping of the local area with the threshold at the origin + // and the runway aligned with the y axis. + + void GetAirportDetails(const string& id); + + void GetRwyDetails(const string& id); + + double responseCounter; // timer in seconds to allow response to requests to be a little while after them + // Will almost certainly get moved to FGAIPlane. + +private: + FGATCMgr* ATC; + // This is purely for synactic convienience to avoid writing globals->get_ATC_mgr()-> all through the code! + + // High-level stuff + OperatingState operatingState; + int circuitsToFly; //Number of circuits still to do in this session NOT INCLUDING THE CURRENT ONE + bool touchAndGo; //True if circuits should be flown touch and go, false for full stop + bool transmitted; // Set true when a position report for the current leg has been transmitted. + + // Performance characteristics of the plane in knots and ft/min - some of this might get moved out into FGAIPlane + double Vr; + double best_rate_of_climb_speed; + double best_rate_of_climb; + double nominal_climb_speed; + double nominal_climb_rate; + double nominal_circuit_speed; + double min_circuit_speed; + double max_circuit_speed; + double nominal_descent_rate; + double nominal_approach_speed; + double nominal_final_speed; + double stall_speed_landing_config; + double nominal_taxi_speed; + + // Physical/rendering stuff + double wheelOffset; // Height above ground at which we need to render the plane whilst taxiing + bool elevInitGood; // We have had at least one good elev reading + bool inAir; // True when off the ground + + // environment - some of this might get moved into FGAIPlane + SGPropertyNode_ptr wind_from_hdg; //degrees + SGPropertyNode_ptr wind_speed_knots; //knots + + // Pattern details that (may) change + int numInPattern; // Number of planes in the pattern (this might get more complicated if high performance GA aircraft fly a higher pattern eventually) + int numAhead; // More importantly - how many of them are ahead of us? + double distToNext; // And even more importantly, how near are we getting to the one immediately ahead? + //PatternLeg leg; // Our current position in the pattern - now moved to FGAIPlane + StartOfDescent SoD; // Start of descent calculated wrt wind, pattern size & altitude, glideslope etc + bool descending; // We're in the coming down phase of the pattern + double targetDescentRate; // m/s + + // Taxiing details + // At the moment this assumes that all taxiing in is to gates (a loose term that includes + // any permitted parking spot) and that all taxiing out is to runways. + bool parked; + bool taxiing; + bool taxiRequestPending; + bool taxiRequestCleared; + TaxiState taxiState; + double desiredTaxiHeading; + double taxiTurnRadius; + double nominalTaxiSpeed; + Gate* ourGate; + ground_network_path_type path; // a path through the ground network for the plane to taxi + unsigned int taxiPathPos; // position of iterator in taxi path when applicable + node* nextTaxiNode; // next node in taxi path + node* holdShortNode; + //Runway out_dest; //FIXME - implement this + bool holdingShort; + bool reportReadyForDeparture; // set true when ATC has requested that the plane report when ready for departure + bool clearedToLineUp; + bool clearedToTakeOff; + bool _clearedToLand; // also implies cleared for the option. + bool liningUp; // Set true when the turn onto the runway heading is commenced when taxiing out + bool goAround; // Set true if need to go-around + bool goAroundCalled; // Set true during go-around only after we have called our go-around on the radio + bool contactTower; // we have been told to contact tower + bool contactGround; // we have been told to contact ground + bool changeFreq; // true when we need to change frequency + bool _taxiToGA; // Temporary mega-hack indicating we are to taxi to the GA parking and disconnect from tower control. + bool _removeSelf; // Indicates that we wish to remove this instance. The use of a variable is a hack to allow time for messages to purge before removal, due to the fagility of the current dialog system. + atc_type changeFreqType; // the service we need to change to + bool freeTaxi; // False if the airport has a facilities file with a logical taxi network defined, true if we need to calculate our own taxiing points. + + // Hack for getting close to the runway when atan can go pear-shaped + double _savedSlope; + + void FlyTrafficPattern(double dt); + + // TODO - need to add something to define what option we are flying - Touch and go / Stop and go / Landing properly / others? + + void TransmitPatternPositionReport(); + + void CalculateSoD(double base_leg_pos, double downwind_leg_pos, bool pattern_direction); + + void ExitRunway(const Point3D& orthopos); + + void StartTaxi(); + + void Taxi(double dt); + + void GetNextTaxiNode(); + + void DoGroundElev(); + + // Set when the plane should be invisible *regardless of distance from user*. + bool _invisible; +}; + +#endif // _FG_AILocalTraffic_HXX diff --git a/src/ATCDCL/AIMgr.cxx b/src/ATCDCL/AIMgr.cxx new file mode 100644 index 000000000..23d44bbf1 --- /dev/null +++ b/src/ATCDCL/AIMgr.cxx @@ -0,0 +1,624 @@ +// AIMgr.cxx - implementation of FGAIMgr +// - a global management class for FlightGear generated AI traffic +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <Main/fg_props.hxx> +#include <Main/globals.hxx> +#include <simgear/misc/sg_path.hxx> +#include <simgear/math/sg_random.h> +#include <simgear/scene/model/modellib.hxx> +#include <list> + +#ifdef _MSC_VER +# include <io.h> +#else +# include <sys/types.h> // for directory reading +# include <dirent.h> // for directory reading +#endif + +#include <Environment/environment_mgr.hxx> +#include <Environment/environment.hxx> + +#include "AIMgr.hxx" +#include "AILocalTraffic.hxx" +#include "AIGAVFRTraffic.hxx" +#include "ATCutils.hxx" +#include "commlist.hxx" + +SG_USING_STD(list); +SG_USING_STD(cout); + +using namespace simgear; + +FGAIMgr::FGAIMgr() { + ATC = globals->get_ATC_mgr(); + initDone = false; + ai_callsigns_used["GFS"] = 1; // so we don't inadvertently use this + // TODO - use the proper user callsign when it becomes user settable. + removalList.clear(); + activated.clear(); + _havePiperModel = true; +} + +FGAIMgr::~FGAIMgr() { + for (ai_list_itr = ai_list.begin(); ai_list_itr != ai_list.end(); ai_list_itr++) { + delete (*ai_list_itr); + } +} + +void FGAIMgr::init() { + //cout << "AIMgr::init called..." << endl; + + // Pointers to user's position + lon_node = fgGetNode("/position/longitude-deg", true); + lat_node = fgGetNode("/position/latitude-deg", true); + elev_node = fgGetNode("/position/altitude-ft", true); + + lon = lon_node->getDoubleValue(); + lat = lat_node->getDoubleValue(); + elev = elev_node->getDoubleValue(); + + // Load up models at the start to avoid pausing later + // Hack alert - Hardwired paths!! + string planepath = "Aircraft/c172p/Models/c172p.xml"; + bool _loadedDefaultOK = true; + try { + _defaultModel = SGModelLib::loadPagedModel(planepath.c_str(), globals->get_props()); + } catch(sg_exception&) { + _loadedDefaultOK = false; + } + + if(!_loadedDefaultOK ) { + // Just load the same 3D model as the default user plane - that's *bound* to exist! + // TODO - implement robust determination of availability of GA AI aircraft models + planepath = "Aircraft/c172p/Models/c172p.ac"; + _defaultModel = SGModelLib::loadPagedModel(planepath.c_str(), globals->get_props()); + } + + planepath = "Aircraft/pa28-161/Models/pa28-161.ac"; + try { + _piperModel = SGModelLib::loadPagedModel(planepath.c_str(), globals->get_props()); + } catch(sg_exception&) { + _havePiperModel = false; + } + + // go through the $FG_ROOT/ATC directory and find all *.taxi files + SGPath path(globals->get_fg_root()); + path.append("ATC/"); + string dir = path.dir(); + string ext; + string file, f_ident; + int pos; + + ulDir *d; + struct ulDirEnt *de; + + if ( (d = ulOpenDir( dir.c_str() )) == NULL ) { + SG_LOG(SG_ATC, SG_WARN, "cannot open directory " << dir); + } else { + // load all .taxi files + while ( (de = ulReadDir(d)) != NULL ) { + file = de->d_name; + pos = file.find("."); + ext = file.substr(pos + 1); + if(ext == "taxi") { + f_ident = file.substr(0, pos); + const FGAirport *a = fgFindAirportID( f_ident); + if(a){ + SGBucket sgb(a->getLongitude(), a->getLatitude()); + int idx = sgb.gen_index(); + if(facilities.find(idx) != facilities.end()) { + facilities[idx]->push_back(f_ident); + } else { + ID_list_type* apts = new ID_list_type; + apts->push_back(f_ident); + facilities[idx] = apts; + } + SG_LOG(SG_ATC, SG_BULK, "Mapping " << f_ident << " to bucket " << idx); + } + } + } + ulCloseDir(d); + } + + // See if are in range at startup and activate if necessary + SearchByPos(15.0); + + initDone = true; + + //cout << "AIMgr::init done..." << endl; + + /* + // TESTING + FGATCAlignedProjection ortho; + ortho.Init(fgGetAirportPos("KEMT"), 205.0); // Guess of rwy19 heading + //Point3D ip = ortho.ConvertFromLocal(Point3D(6000, 1000, 1000)); // 90 deg entry + //Point3D ip = ortho.ConvertFromLocal(Point3D(-7000, 3000, 1000)); // 45 deg entry + Point3D ip = ortho.ConvertFromLocal(Point3D(1000, -7000, 1000)); // straight-in + ATC->AIRegisterAirport("KEMT"); + FGAIGAVFRTraffic* p = new FGAIGAVFRTraffic(); + p->SetModel(_defaultModel); + p->Init(ip, "KEMT", GenerateShortForm(GenerateUniqueCallsign())); + ai_list.push_back(p); + traffic[ident].push_back(p); + activated["KEMT"] = 1; + */ +} + +void FGAIMgr::bind() { +} + +void FGAIMgr::unbind() { +} + +void FGAIMgr::update(double dt) { + if(!initDone) { + init(); + SG_LOG(SG_ATC, SG_WARN, "Warning - AIMgr::update(...) called before AIMgr::init()"); + } + + //cout << activated.size() << '\n'; + + Point3D userPos = Point3D(lon_node->getDoubleValue(), lat_node->getDoubleValue(), elev_node->getDoubleValue()); + + // TODO - make these class variables!! + static int i = 0; + static int j = 0; + + // Don't update any planes for first 50 runs through - this avoids some possible initialisation anomalies + // Might not need it now we have fade-in though? + if(i < 50) { + ++i; + return; + } + + if(j == 215) { + SearchByPos(25.0); + j = 0; + } else if(j == 200) { + // Go through the list of activated airports and remove those out of range + //cout << "The following airports have been activated by the AI system:\n"; + ai_activated_map_iterator apt_itr = activated.begin(); + while(apt_itr != activated.end()) { + //cout << "FIRST IS " << (*apt_itr).first << '\n'; + if(dclGetHorizontalSeparation(userPos, fgGetAirportPos((*apt_itr).first)) > (35.0 * 1600.0)) { + // Then get rid of it and make sure the iterator is left pointing to the next one! + string s = (*apt_itr).first; + if(traffic.find(s) != traffic.end()) { + //cout << "s = " << s << ", traffic[s].size() = " << traffic[s].size() << '\n'; + if(!traffic[s].empty()) { + apt_itr++; + } else { + //cout << "Erasing " << (*apt_itr).first << " and traffic" << '\n'; + activated.erase(apt_itr); + apt_itr = activated.upper_bound(s); + traffic.erase(s); + } + } else { + //cout << "Erasing " << (*apt_itr).first << ' ' << (*apt_itr).second << '\n'; + activated.erase(apt_itr); + apt_itr = activated.upper_bound(s); + } + } else { + apt_itr++; + } + } + } else if(j == 180) { + // Go through the list of activated airports and do the random airplane generation + ai_traffic_map_iterator it = traffic.begin(); + while(it != traffic.end()) { + string s = (*it).first; + //cout << "s = " << s << " size = " << (*it).second.size() << '\n'; + // Only generate extra traffic if within a certain distance of the user, + // TODO - maybe take users's tuned freq into account as well. + double d = dclGetHorizontalSeparation(userPos, fgGetAirportPos(s)); + if(d < (15.0 * 1600.0)) { + double cd = 0.0; + bool gen = false; + //cout << "Size of list is " << (*it).second.size() << " at " << s << '\n'; + if((*it).second.size()) { + FGAIEntity* e = *((*it).second.rbegin()); // Get the last airplane currently scheduled to arrive at this airport. + cd = dclGetHorizontalSeparation(e->GetPos(), fgGetAirportPos(s)); + if(cd < (d < 5000 ? 10000 : d + 5000)) { + gen = true; + } + } else { + gen = true; + cd = 0.0; + } + if(gen) { + //cout << "Generating extra traffic at airport " << s << ", at least " << cd << " meters out\n"; + //GenerateSimpleAirportTraffic(s, cd); + GenerateSimpleAirportTraffic(s, cd + 3000.0); // The random seems a bit wierd - traffic could get far too bunched without the +3000. + // TODO - make the anti-random constant variable depending on the ai-traffic level. + } + } + ++it; + } + } + + ++j; + + //cout << "Size of AI list is " << ai_list.size() << '\n'; + + // TODO - need to add a check of if any activated airports have gone out of range + + string rs; // plane to be removed, if one. + if(removalList.size()) { + rs = *(removalList.begin()); + removalList.pop_front(); + } else { + rs = ""; + } + + // Traverse the list of active planes and run all their update methods + // TODO - spread the load - not all planes should need updating every frame. + // Note that this will require dt to be calculated for each plane though + // since they rely on it to calculate distance travelled. + ai_list_itr = ai_list.begin(); + while(ai_list_itr != ai_list.end()) { + FGAIEntity *e = *ai_list_itr; + if(rs.size() && e->GetCallsign() == rs) { + //cout << "Removing " << rs << " from ai_list\n"; + ai_list_itr = ai_list.erase(ai_list_itr); + delete e; + // This is a hack - we should deref this plane from the airport count! + } else { + e->Update(dt); + ++ai_list_itr; + } + } + + //cout << "Size of AI list is " << ai_list.size() << '\n'; +} + +void FGAIMgr::ScheduleRemoval(const string& s) { + //cout << "Scheduling removal of plane " << s << " from AIMgr\n"; + removalList.push_back(s); +} + +// Activate AI traffic at an airport +void FGAIMgr::ActivateAirport(const string& ident) { + ATC->AIRegisterAirport(ident); + // TODO - need to start the traffic more randomly + FGAILocalTraffic* local_traffic = new FGAILocalTraffic; + local_traffic->SetModel(_defaultModel.get()); // currently hardwired to cessna. + //local_traffic->Init(ident, IN_PATTERN, TAKEOFF_ROLL); + local_traffic->Init(GenerateShortForm(GenerateUniqueCallsign()), ident); + local_traffic->FlyCircuits(1, true); // Fly 2 circuits with touch & go in between + ai_list.push_back(local_traffic); + traffic[ident].push_back(local_traffic); + //cout << "******** ACTIVATING AIRPORT, ident = " << ident << '\n'; + activated[ident] = 1; +} + +// Hack - Generate AI traffic at an airport with no facilities file +void FGAIMgr::GenerateSimpleAirportTraffic(const string& ident, double min_dist) { + // Ugly hack - don't let VFR Cessnas operate at a hardwired list of major airports + // This will go eventually once airport .xml files specify the traffic profile + if(ident == "KSFO" || ident == "KDFW" || ident == "EGLL" || ident == "KORD" || ident == "KJFK" + || ident == "KMSP" || ident == "KLAX" || ident == "KBOS" || ident == "KEDW" + || ident == "KSEA" || ident == "EHAM") { + return; + } + + /* + // TODO - check for military airports - this should be in the current data. + // UGGH - there's no point at the moment - everything is labelled civil in basic.dat! + FGAirport a = fgFindAirportID(ident, &a); + if(a) { + cout << "CODE IS " << a.code << '\n'; + } else { + // UG - can't find the airport! + return; + } + */ + + Point3D aptpos = fgGetAirportPos(ident); // TODO - check for elev of -9999 + //cout << "ident = " << ident << ", elev = " << aptpos.elev() << '\n'; + + // Operate from airports at 3000ft and below only to avoid the default cloud layers and since we don't degrade AI performance with altitude. + if(aptpos.elev() > 3000) { + //cout << "High alt airports not yet supported - returning\n"; + return; + } + + // Rough hack for plane type - make 70% of the planes cessnas, the rest pipers. + bool cessna = true; + + // Get the time and only operate VFR in the (approximate) daytime. + struct tm *t = globals->get_time_params()->getGmt(); + int loc_time = t->tm_hour + int(aptpos.lon() / (360.0 / 24.0)); + while (loc_time < 0) + loc_time += 24; + while (loc_time >= 24) + loc_time -= 24; + + //cout << "loc_time = " << loc_time << '\n'; + if(loc_time < 7 || loc_time > 19) return; + + // Check that the visibility is OK for IFR operation. + double visibility; + FGEnvironment stationweather = + ((FGEnvironmentMgr *)globals->get_subsystem("environment")) + ->getEnvironment(aptpos.lat(), aptpos.lon(), aptpos.elev()); // TODO - check whether this should take ft or m for elev. + visibility = stationweather.get_visibility_m(); + // Technically we can do VFR down to 1 mile (1600m) but that's pretty murky! + //cout << "vis = " << visibility << '\n'; + if(visibility < 3000) return; + + ATC->AIRegisterAirport(ident); + + // Next - get the distance from user to the airport. + Point3D userpos = Point3D(lon_node->getDoubleValue(), lat_node->getDoubleValue(), elev_node->getDoubleValue()); + double d = dclGetHorizontalSeparation(userpos, aptpos); // in meters + + int lev = fgGetInt("/sim/ai-traffic/level"); + if(lev < 1) + return; + if (lev > 3) + lev = 3; + if(visibility < 6000) lev = 1; + //cout << "level = " << lev << '\n'; + + // Next - generate any local / circuit traffic + + /* + // --------------------------- THIS BLOCK IS JUST FOR TESTING - COMMENT OUT BEFORE RELEASE --------------- + // Finally - generate VFR approaching traffic + //if(d > 2000) { + if(ident == "KPOC") { + double ad = 2000.0; + double avd = 3000.0; // average spacing of arriving traffic in meters - relate to airport business and AI density setting one day! + //while(ad < (d < 10000 ? 12000 : d + 2000)) { + for(int i=0; i<8; ++i) { + double dd = sg_random() * avd; + // put a minimum spacing in for now since I don't think tower will cope otherwise! + if(dd < 1500) dd = 1500; + //ad += dd; + ad += dd; + double dir = int(sg_random() * 36); + if(dir == 36) dir--; + dir *= 10; + //dir = 180; + if(sg_random() < 0.3) cessna = false; + else cessna = true; + string s = GenerateShortForm(GenerateUniqueCallsign(), (cessna ? "Cessna-" : "Piper-")); + FGAIGAVFRTraffic* t = new FGAIGAVFRTraffic(); + t->SetModel(cessna ? _defaultModel : _piperModel); + //cout << "Generating VFR traffic " << s << " inbound to " << ident << " " << ad << " meters out from " << dir << " degrees\n"; + Point3D tpos = dclUpdatePosition(aptpos, dir, 6.0, ad); + if(tpos.elev() > (aptpos.elev() + 3000.0)) tpos.setelev(aptpos.elev() + 3000.0); + t->Init(tpos, ident, s); + ai_list.push_back(t); + } + } + activated[ident] = 1; + return; + //--------------------------------------------------------------------------------------------------- + */ + + double ad; // Minimum distance out of first arriving plane in meters. + double mind; // Minimum spacing of traffic in meters + double avd; // average spacing of arriving traffic in meters - relate to airport business and AI density setting one day! + // Finally - generate VFR approaching traffic + //if(d > 2000) { + if(1) { + if(lev == 3) { + ad = 5000.0; + mind = 2000.0; + avd = 6000.0; + } else if(lev == 2) { + ad = 8000.0; + mind = 4000.0; + avd = 10000.0; + } else { + ad = 9000.0; // Start the first aircraft at least 9K out for now. + mind = 6000.0; + avd = 15000.0; + } + /* + // Check if there is already arriving traffic at this airport + cout << "BING A " << ident << '\n'; + if(traffic.find(ident) != traffic.end()) { + cout << "BING B " << ident << '\n'; + ai_list_type lst = traffic[ident]; + cout << "BING C " << ident << '\n'; + if(lst.size()) { + cout << "BING D " << ident << '\n'; + double cd = dclGetHorizontalSeparation(aptpos, (*lst.rbegin())->GetPos()); + cout << "ident = " << ident << ", cd = " << cd << '\n'; + if(cd > ad) ad = cd; + } + } + */ + if(min_dist != 0) ad = min_dist; + //cout << "ident = " << ident << ", ad = " << ad << '\n'; + while(ad < (d < 5000 ? 15000 : d + 10000)) { + double dd = mind + (sg_random() * (avd - mind)); + ad += dd; + double dir = int(sg_random() * 36); + if(dir == 36) dir--; + dir *= 10; + + if(sg_random() < 0.3) cessna = false; + else cessna = true; + string s = GenerateShortForm(GenerateUniqueCallsign(), (cessna ? "Cessna-" : "Piper-")); + FGAIGAVFRTraffic* t = new FGAIGAVFRTraffic(); + t->SetModel(cessna ? _defaultModel.get() : (_havePiperModel ? _piperModel.get() : _defaultModel.get())); + //cout << "Generating VFR traffic " << s << " inbound to " << ident << " " << ad << " meters out from " << dir << " degrees\n"; + Point3D tpos = dclUpdatePosition(aptpos, dir, 6.0, ad); + if(tpos.elev() > (aptpos.elev() + 3000.0)) tpos.setelev(aptpos.elev() + 3000.0); // FEET yuk :-( + t->Init(tpos, ident, s); + ai_list.push_back(t); + traffic[ident].push_back(t); + } + } +} + +/* +// Generate a VFR arrival at airport apt, at least distance d (meters) out. +void FGAIMgr::GenerateVFRArrival(const string& apt, double d) { +} +*/ + +// Search for valid airports in the vicinity of the user and activate them if necessary +void FGAIMgr::SearchByPos(double range) { + //cout << "In SearchByPos(...)" << endl; + + // get bucket number for plane position + lon = lon_node->getDoubleValue(); + lat = lat_node->getDoubleValue(); + elev = elev_node->getDoubleValue() * SG_FEET_TO_METER; + SGBucket buck(lon, lat); + + // get neigboring buckets + int bx = (int)( range*SG_NM_TO_METER / buck.get_width_m() / 2); + //cout << "bx = " << bx << endl; + int by = (int)( range*SG_NM_TO_METER / buck.get_height_m() / 2 ); + //cout << "by = " << by << endl; + + // Search for airports with facitities files -------------------------- + // loop over bucket range + for ( int i=-bx; i<=bx; i++) { + //cout << "i loop\n"; + for ( int j=-by; j<=by; j++) { + //cout << "j loop\n"; + buck = sgBucketOffset(lon, lat, i, j); + long int bucket = buck.gen_index(); + //cout << "bucket is " << bucket << endl; + if(facilities.find(bucket) != facilities.end()) { + ID_list_type* apts = facilities[bucket]; + ID_list_iterator current = apts->begin(); + ID_list_iterator last = apts->end(); + + //cout << "Size of apts is " << apts->size() << endl; + + //double rlon = lon * SGD_DEGREES_TO_RADIANS; + //double rlat = lat * SGD_DEGREES_TO_RADIANS; + //Point3D aircraft = sgGeodToCart( Point3D(rlon, rlat, elev) ); + //Point3D airport; + for(; current != last; ++current) { + //cout << "Found " << *current << endl;; + if(activated.find(*current) == activated.end()) { + //cout << "Activating " << *current << endl; + //FGAirport a; + //if(dclFindAirportID(*current, &a)) { + // // We can do something here based on distance from the user if we wish. + //} + //string s = *current; + //cout << "s = " << s << '\n'; + ActivateAirport(*current); + //ActivateSimpleAirport(*current); // TODO - put this back to ActivateAirport when that code is done. + //cout << "Activation done" << endl; + } else { + //cout << *current << " already activated" << endl; + } + } + } + } + } + //------------------------------------------------------------- + + // Search for any towered airports in the vicinity ------------ + comm_list_type towered; + comm_list_iterator twd_itr; + + int num_twd = current_commlist->FindByPos(lon, lat, elev, range, &towered, TOWER); + if (num_twd != 0) { + double closest = 1000000; + string s = ""; + for(twd_itr = towered.begin(); twd_itr != towered.end(); twd_itr++) { + // Only activate the closest airport not already activated each time. + if(activated.find(twd_itr->ident) == activated.end()) { + double sep = dclGetHorizontalSeparation(Point3D(lon, lat, elev), fgGetAirportPos(twd_itr->ident)); + if(sep < closest) { + closest = sep; + s = twd_itr->ident; + } + + } + } + if(s.size()) { + // TODO - find out why empty strings come through here when all in-range airports done. + GenerateSimpleAirportTraffic(s); + //cout << "**************ACTIVATING SIMPLE AIRPORT, ident = " << s << '\n'; + activated[s] = 1; + } + } +} + +string FGAIMgr::GenerateCallsign() { + // For now we'll just generate US callsigns until we can regionally identify airports. + string s = "N"; + // Add 3 to 5 numbers and make up to 5 with letters. + //sg_srandom_time(); + double d = sg_random(); + int n = int(d * 3); + if(n == 3) --n; + //cout << "First n, n = " << n << '\n'; + int j = 3 + n; + //cout << "j = " << j << '\n'; + for(int i=0; i<j; ++i) { + int n = int(sg_random() * 10); + if(n == 10) --n; + s += (char)('0' + n); + } + for(int i=j; i<5; ++i) { + int n = int(sg_random() * 26); + if(n == 26) --n; + //cout << "Alpha, n = " << n << '\n'; + s += (char)('A' + n); + } + //cout << "s = " << s << '\n'; + return(s); +} + +string FGAIMgr::GenerateUniqueCallsign() { + while(1) { + string s = GenerateCallsign(); + if(!ai_callsigns_used[s]) { + ai_callsigns_used[s] = 1; + return(s); + } + } +} + +// This will be moved somewhere else eventually!!!! +string FGAIMgr::GenerateShortForm(const string& callsign, const string& plane_str, bool local) { + //cout << callsign << '\n'; + string s; + if(local) s = "Trainer-"; + else s = plane_str; + for(int i=3; i>0; --i) { + char c = callsign[callsign.size() - i]; + //cout << c << '\n'; + string tmp = ""; + tmp += c; + if(isalpha(c)) s += GetPhoneticIdent(c); + else s += ConvertNumToSpokenDigits(tmp); + if(i > 1) s += '-'; + } + return(s); +} diff --git a/src/ATCDCL/AIMgr.hxx b/src/ATCDCL/AIMgr.hxx new file mode 100644 index 000000000..6bc81a10b --- /dev/null +++ b/src/ATCDCL/AIMgr.hxx @@ -0,0 +1,139 @@ +// AIMgr.hxx - definition of FGAIMgr +// - a global management class for FlightGear generated AI traffic +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_AIMGR_HXX +#define _FG_AIMGR_HXX + +#include <simgear/structure/subsystem_mgr.hxx> + +#include <Main/fg_props.hxx> + +#include <list> + +#include "ATCmgr.hxx" +#include "AIEntity.hxx" + +SG_USING_STD(list); + + +class FGAIMgr : public SGSubsystem +{ + +private: + FGATCMgr* ATC; + // This is purely for synactic convienience to avoid writing globals->get_ATC_mgr()-> all through the code! + + // A list of pointers to all currently active AI stuff + typedef list <FGAIEntity*> ai_list_type; + typedef ai_list_type::iterator ai_list_iterator; + typedef ai_list_type::const_iterator ai_list_const_iterator; + + // Everything put in this list should be created dynamically + // on the heap and ***DELETED WHEN REMOVED!!!!!*** + ai_list_type ai_list; + ai_list_iterator ai_list_itr; + // Any member function of FGATCMgr is permitted to leave this iterator pointing + // at any point in or at the end of the list. + // Hence any new access must explicitly first check for atc_list.end() before dereferencing. + + // A list of airport or airplane ID's + typedef list < string > ID_list_type; + typedef ID_list_type::iterator ID_list_iterator; + + // Temporary storage of ID of planes scheduled for removeal + ID_list_type removalList; + + // A map of airport-IDs that have taxiway network files against bucket number + typedef map < int, ID_list_type* > ai_apt_map_type; + typedef ai_apt_map_type::iterator ai_apt_map_iterator; + ai_apt_map_type facilities; + + // A map of airport ID's that we've activated AI traffic at + typedef map < string, int > ai_activated_map_type; + typedef ai_activated_map_type::iterator ai_activated_map_iterator; + ai_activated_map_type activated; + + // AI traffic lists mapped by airport + typedef map < string, ai_list_type > ai_traffic_map_type; + typedef ai_traffic_map_type::iterator ai_traffic_map_iterator; + ai_traffic_map_type traffic; + + // A map of callsigns that we have used (eg CFGFS or N0546D - the code will generate Cessna-four-six-delta from this later) + typedef map < string, int > ai_callsigns_map_type; + typedef ai_callsigns_map_type::iterator ai_callsigns_map_iterator; + ai_callsigns_map_type ai_callsigns_used; + + // Position of the Users Aircraft + double lon; + double lat; + double elev; + // Pointers to current users position + SGPropertyNode_ptr lon_node; + SGPropertyNode_ptr lat_node; + SGPropertyNode_ptr elev_node; + +public: + + FGAIMgr(); + ~FGAIMgr(); + + void init(); + + void bind(); + + void unbind(); + + void update(double dt); + + // Signal that it is OK to remove a plane of callsign s + // (To be called by the plane itself). + void ScheduleRemoval(const string& s); + +private: + + osg::ref_ptr<osg::Node> _defaultModel; // Cessna 172! + osg::ref_ptr<osg::Node> _piperModel; // pa28-161 + + bool initDone; // Hack - guard against update getting called before init + + // Remove a class from the ai_list and delete it from memory + //void RemoveFromList(const char* id, atc_type tp); + + // Activate AI traffic at an airport + void ActivateAirport(const string& ident); + + // Hack - Generate AI traffic at an airport with no facilities file, with the first plane being at least min_dist out. + void GenerateSimpleAirportTraffic(const string& ident, double min_dist = 0.0); + + // Search for valid airports in the vicinity of the user and activate them if necessary + void SearchByPos(double range); + + string GenerateCallsign(); + + string GenerateUniqueCallsign(); + + string GenerateShortForm(const string& callsign, const string& plane_str = "Cessna-", bool local = false); + + // TODO - implement a proper robust system for registering and loading AI GA aircraft models + bool _havePiperModel; +}; + +#endif // _FG_AIMGR_HXX diff --git a/src/ATCDCL/AIPlane.cxx b/src/ATCDCL/AIPlane.cxx new file mode 100644 index 000000000..84006294d --- /dev/null +++ b/src/ATCDCL/AIPlane.cxx @@ -0,0 +1,272 @@ +// FGAIPlane - abstract base class for an AI plane +// +// Written by David Luff, started 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include <Main/globals.hxx> +#include <Main/fg_props.hxx> +#include <simgear/math/point3d.hxx> +#include <simgear/debug/logstream.hxx> +#include <simgear/sound/soundmgr_openal.hxx> +#include <math.h> +#include <string> +SG_USING_STD(string); + + +#include "AIPlane.hxx" + +FGAIPlane::FGAIPlane() { + leg = LEG_UNKNOWN; + tuned_station = NULL; + pending_transmission = ""; + _timeout = 0; + _pending = false; + _callback_code = 0; + _transmit = false; + _transmitting = false; + voice = false; + playing = false; + voiceOK = false; + vPtr = NULL; + track = 0.0; + _tgtTrack = 0.0; + _trackSet = false; + _tgtRoll = 0.0; + _rollSuspended = false; +} + +FGAIPlane::~FGAIPlane() { +} + +void FGAIPlane::Update(double dt) { + if(_pending) { + if(tuned_station) { + if(tuned_station->GetFreqClear()) { + //cout << "TUNED STATION FREQ CLEAR\n"; + tuned_station->SetFreqInUse(); + _pending = false; + _transmit = true; + _transmitting = false; + } else { + if(_timeout > 0.0) { // allows count down to be avoided by initially setting it to zero + _timeout -= dt; + if(_timeout <= 0.0) { + _timeout = 0.0; + _pending = false; + // timed out - don't render. + if(_callback_code == 99) { + // MEGA-HACK - 99 is the remove self callback - currently this *does* need to be run even if the transmission isn't made. + ProcessCallback(_callback_code); + } + } + } + } + } else { + // Not tuned to ATC - Just go ahead and transmit + //cout << "NOT TUNED TO ATC\n"; + _pending = false; + _transmit = true; + _transmitting = false; + } + } + + // This turns on rendering if on the same freq as the user + // TODO - turn it off if user switches to another freq - keep track of where in message we are etc. + if(_transmit) { + //cout << "transmit\n"; + double user_freq0 = fgGetDouble("/instrumentation/comm[0]/frequencies/selected-mhz"); + double user_freq1 = fgGetDouble("/instrumentation/comm[1]/frequencies/selected-mhz"); + _counter = 0.0; + _max_count = 5.0; // FIXME - hardwired length of message - need to calculate it! + + //cout << "Transmission = " << pending_transmission << '\n'; + + // The radios dialog seems to set slightly imprecise freqs, eg 118.099998 + // The eplison stuff below is a work-around + double eps0 = fabs(freq - user_freq0); + double eps1 = fabs(freq - user_freq1); + if(eps0 < 0.002 || eps1 < 0.002) { + //cout << "Transmitting..." << endl; + // we are on the same frequency, so check distance to the user plane + if(1) { + // For now assume in range !!! + // TODO - implement range checking + Render(plane.callsign, false); + } + } + // Run the callback regardless of whether on same freq as user or not. + if(_callback_code) { + ProcessCallback(_callback_code); + } + _transmit = false; + _transmitting = true; + } else if(_transmitting) { + if(_counter >= _max_count) { + NoRender(plane.callsign); + _transmitting = false; + // For now we'll let ATC decide whether to respond + //if(tuned_station) tuned_station->SetResponseReqd(plane.callsign); + //if(tuned_station->get_ident() == "KRHV") cout << "Notifying transmission finished" << endl; + if(tuned_station) tuned_station->NotifyTransmissionFinished(plane.callsign); + } + _counter += dt; + } + + // Fly the plane if necessary + if(_trackSet) { + while((_tgtTrack - track) > 180.0) track += 360.0; + while((track - _tgtTrack) > 180.0) track -= 360.0; + double turn_time = 60.0; + track += (360.0 / turn_time) * dt * (_tgtTrack > track ? 1.0 : -1.0); + // TODO - bank a bit less for small turns. + Bank(25.0 * (_tgtTrack > track ? 1.0 : -1.0)); + if(fabs(track - _tgtTrack) < 2.0) { // TODO - might need to optimise the delta there - it's on the large (safe) side atm. + track = _tgtTrack; + LevelWings(); + } + } + + if(!_rollSuspended) { + if(fabs(_roll - _tgtRoll) > 0.6) { + // This *should* bank us smoothly to any angle + _roll -= ((_roll - _tgtRoll)/fabs(_roll - _tgtRoll)); + } else { + _roll = _tgtRoll; + } + } +} + +void FGAIPlane::Transmit(int callback_code) { + SG_LOG(SG_ATC, SG_INFO, "Transmit called for plane " << plane.callsign << ", msg = " << pending_transmission); + _pending = true; + _callback_code = callback_code; + _timeout = 0.0; +} + +void FGAIPlane::ConditionalTransmit(double timeout, int callback_code) { + SG_LOG(SG_ATC, SG_INFO, "Timed transmit called for plane " << plane.callsign << ", msg = " << pending_transmission); + _pending = true; + _callback_code = callback_code; + _timeout = timeout; +} + +void FGAIPlane::ImmediateTransmit(int callback_code) { + Render(plane.callsign, false); + if(callback_code) { + ProcessCallback(callback_code); + } +} + +// Derived classes should override this. +void FGAIPlane::ProcessCallback(int code) { +} + +// Render a transmission +// Outputs the transmission either on screen or as audio depending on user preference +// The refname is a string to identify this sample to the sound manager +// The repeating flag indicates whether the message should be repeated continuously or played once. +void FGAIPlane::Render(const string& refname, bool repeating) { + fgSetString("/sim/messages/ai-plane", pending_transmission.c_str()); +#ifdef ENABLE_AUDIO_SUPPORT + voice = (voiceOK && fgGetBool("/sim/sound/voice")); + if(voice) { + int len; + unsigned char* buf = vPtr->WriteMessage((char*)pending_transmission.c_str(), len, voice); + if(voice) { + SGSoundSample* simple = new SGSoundSample(buf, len, 8000); + // TODO - at the moment the volume is always set off comm1 + // and can't be changed after the transmission has started. + simple->set_volume(5.0 * fgGetDouble("/instrumentation/comm[0]/volume")); + globals->get_soundmgr()->add(simple, refname); + if(repeating) { + globals->get_soundmgr()->play_looped(refname); + } else { + globals->get_soundmgr()->play_once(refname); + } + } + delete[] buf; + } +#endif // ENABLE_AUDIO_SUPPORT + if(!voice) { + // first rip the underscores and the pause hints out of the string - these are for the convienience of the voice parser + for(unsigned int i = 0; i < pending_transmission.length(); ++i) { + if((pending_transmission.substr(i,1) == "_") || (pending_transmission.substr(i,1) == "/")) { + pending_transmission[i] = ' '; + } + } + } + playing = true; +} + + +// Cease rendering a transmission. +void FGAIPlane::NoRender(const string& refname) { + if(playing) { + if(voice) { +#ifdef ENABLE_AUDIO_SUPPORT + globals->get_soundmgr()->stop(refname); + globals->get_soundmgr()->remove(refname); +#endif + } + playing = false; + } +} + +/* + +*/ + +void FGAIPlane::RegisterTransmission(int code) { +} + + +// Return what type of landing we're doing on this circuit +LandingType FGAIPlane::GetLandingOption() { + return(FULL_STOP); +} + + +ostream& operator << (ostream& os, PatternLeg pl) { + switch(pl) { + case(TAKEOFF_ROLL): return(os << "TAKEOFF ROLL"); + case(CLIMBOUT): return(os << "CLIMBOUT"); + case(TURN1): return(os << "TURN1"); + case(CROSSWIND): return(os << "CROSSWIND"); + case(TURN2): return(os << "TURN2"); + case(DOWNWIND): return(os << "DOWNWIND"); + case(TURN3): return(os << "TURN3"); + case(BASE): return(os << "BASE"); + case(TURN4): return(os << "TURN4"); + case(FINAL): return(os << "FINAL"); + case(LANDING_ROLL): return(os << "LANDING ROLL"); + case(LEG_UNKNOWN): return(os << "UNKNOWN"); + } + return(os << "ERROR - Unknown switch in PatternLeg operator << "); +} + + +ostream& operator << (ostream& os, LandingType lt) { + switch(lt) { + case(FULL_STOP): return(os << "FULL STOP"); + case(STOP_AND_GO): return(os << "STOP AND GO"); + case(TOUCH_AND_GO): return(os << "TOUCH AND GO"); + case(AIP_LT_UNKNOWN): return(os << "UNKNOWN"); + } + return(os << "ERROR - Unknown switch in LandingType operator << "); +} + diff --git a/src/ATCDCL/AIPlane.hxx b/src/ATCDCL/AIPlane.hxx new file mode 100644 index 000000000..b29726c39 --- /dev/null +++ b/src/ATCDCL/AIPlane.hxx @@ -0,0 +1,165 @@ +// FGAIPlane - abstract base class for an AI plane +// +// Written by David Luff, started 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_AI_PLANE_HXX +#define _FG_AI_PLANE_HXX + +#include <simgear/math/point3d.hxx> + +#include "AIEntity.hxx" +#include "ATC.hxx" + +enum PatternLeg { + TAKEOFF_ROLL, + CLIMBOUT, + TURN1, + CROSSWIND, + TURN2, + DOWNWIND, + TURN3, + BASE, + TURN4, + FINAL, + LANDING_ROLL, + LEG_UNKNOWN +}; + +ostream& operator << (ostream& os, PatternLeg pl); + +enum LandingType { + FULL_STOP, + STOP_AND_GO, + TOUCH_AND_GO, + AIP_LT_UNKNOWN +}; + +ostream& operator << (ostream& os, LandingType lt); + +/***************************************************************** +* +* FGAIPlane - this class is derived from FGAIEntity and adds the +* practical requirement for an AI plane - the ability to send radio +* communication, and simple performance details for the actual AI +* implementation to use. The AI implementation is expected to be +* in derived classes - this class does nothing useful on its own. +* +******************************************************************/ +class FGAIPlane : public FGAIEntity { + +public: + + FGAIPlane(); + virtual ~FGAIPlane(); + + // Run the internal calculations + void Update(double dt); + + // Send a transmission *TO* the AIPlane. + // FIXME int code is a hack - eventually this will receive Alexander's coded messages. + virtual void RegisterTransmission(int code); + + // Return the current pattern leg the plane is flying. + inline PatternLeg GetLeg() {return leg;} + + // Return what type of landing we're doing on this circuit + virtual LandingType GetLandingOption(); + + // Return the callsign + inline const string& GetCallsign() {return plane.callsign;} + +protected: + PlaneRec plane; + + double mag_hdg; // degrees - the heading that the physical aircraft is *pointing* + double track; // track that the physical aircraft is *following* - degrees relative to *true* north + double crab; // Difference between heading and track due to wind. + double mag_var; // degrees + double IAS; // Indicated airspeed in knots + double vel; // velocity along track in knots + double vel_si; // velocity along track in m/s + double slope; // Actual slope that the plane is flying (degrees) - +ve is uphill + double AoA; // degrees - difference between slope and pitch + // We'll assume that the plane doesn't yaw or slip - the track/heading difference is to allow for wind + + double freq; // The comm frequency that we're operating on + + // We need some way for this class to display its radio transmissions if on the + // same frequency and in the vicinity of the user's aircraft + // This may need to be done independently of ATC eg CTAF + // Make radio transmission - this simply sends the transmission for physical rendering if the users + // aircraft is on the same frequency and in range. It is up to the derived classes to let ATC know + // what is going on. + string pending_transmission; // derived classes set this string before calling Transmit(...) + FGATC* tuned_station; // and this if they are tuned to ATC + + // Transmit a message when channel becomes free of other dialog + void Transmit(int callback_code = 0); + + // Transmit a message if channel becomes free within timeout (seconds). timeout of zero implies no limit + void ConditionalTransmit(double timeout, int callback_code = 0); + + // Transmit regardless of other dialog on the channel eg emergency + void ImmediateTransmit(int callback_code = 0); + + inline void SetTrack(double t) { _tgtTrack = t; _trackSet = true; } + inline void ClearTrack() { _trackSet = false; } + + inline void Bank(double r) { _tgtRoll = r; } + inline void LevelWings(void) { _tgtRoll = 0.0; } + + virtual void ProcessCallback(int code); + + PatternLeg leg; + +private: + bool _pending; + double _timeout; + int _callback_code; // A callback code to be notified and processed by the derived classes + // A value of zero indicates no callback required + bool _transmit; // we are to transmit + bool _transmitting; // we are transmitting + double _counter; + double _max_count; + + // Render a transmission (in string pending_transmission) + // Outputs the transmission either on screen or as audio depending on user preference + // The refname is a string to identify this sample to the sound manager + // The repeating flag indicates whether the message should be repeated continuously or played once. + void Render(const string& refname, bool repeating); + + // Cease rendering a transmission. + // Requires the sound manager refname if audio, else "". + void NoRender(const string& refname); + + // Rendering related stuff + bool voice; // Flag - true if we are using voice + bool playing; // Indicates a message in progress + bool voiceOK; // Flag - true if at least one voice has loaded OK + FGATCVoice* vPtr; + + // Navigation + double _tgtTrack; // Track to be following if _trackSet is true + bool _trackSet; // Set true if tgtTrack is to be followed + double _tgtRoll; + bool _rollSuspended; // Set true when a derived class has suspended AIPlane's roll control +}; + +#endif // _FG_AI_PLANE_HXX + diff --git a/src/ATCDCL/ATC.cxx b/src/ATCDCL/ATC.cxx new file mode 100644 index 000000000..2bc321805 --- /dev/null +++ b/src/ATCDCL/ATC.cxx @@ -0,0 +1,291 @@ +// Implementation of FGATC - ATC subsystem base class. +// +// Written by David Luff, started February 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/sound/soundmgr_openal.hxx> +#include <simgear/structure/exception.hxx> + +#include <Main/globals.hxx> +#include <Main/fg_props.hxx> + +#include "ATC.hxx" + +FGATC::FGATC() { + freqClear = true; + receiving = false; + respond = false; + runResponseCounter = false; + _runReleaseCounter = false; + responseID = ""; + responseReqd = false; + _type = INVALID; + _display = false; + _displaying = false; + + // Transmission timing stuff + pending_transmission = ""; + _timeout = 0; + _pending = false; + _callback_code = 0; + _transmit = false; + _transmitting = false; + _counter = 0.0; + _max_count = 5.0; + + _voiceOK = false; +} + +FGATC::~FGATC() { +} + +// Derived classes wishing to use the response counter should call this from their own Update(...). +void FGATC::Update(double dt) { + if(runResponseCounter) { + //cout << responseCounter << '\t' << responseTime << '\n'; + if(responseCounter >= responseTime) { + runResponseCounter = false; + respond = true; + //cout << "RESPOND\n"; + } else { + responseCounter += dt; + } + } + + if(_runReleaseCounter) { + if(_releaseCounter >= _releaseTime) { + freqClear = true; + _runReleaseCounter = false; + } else { + _releaseCounter += dt; + } + } + + // Transmission stuff cribbed from AIPlane.cxx + if(_pending) { + if(GetFreqClear()) { + //cout << "TUNED STATION FREQ CLEAR\n"; + SetFreqInUse(); + _pending = false; + _transmit = true; + _transmitting = false; + } else { + if(_timeout > 0.0) { // allows count down to be avoided by initially setting it to zero + _timeout -= dt; + if(_timeout <= 0.0) { + _timeout = 0.0; + _pending = false; + // timed out - don't render. + } + } + } + } + + if(_transmit) { + _counter = 0.0; + _max_count = 5.0; // FIXME - hardwired length of message - need to calculate it! + + //cout << "Transmission = " << pending_transmission << '\n'; + if(_display) { + //Render(pending_transmission, ident, false); + Render(pending_transmission); + } + // Run the callback regardless of whether on same freq as user or not. + //cout << "_callback_code = " << _callback_code << '\n'; + if(_callback_code) { + ProcessCallback(_callback_code); + } + _transmit = false; + _transmitting = true; + } else if(_transmitting) { + if(_counter >= _max_count) { + //NoRender(plane.callsign); commented out since at the moment NoRender is designed just to stop repeating messages, + // and this will be primarily used on single messages. + _transmitting = false; + //if(tuned_station) tuned_station->NotifyTransmissionFinished(plane.callsign); + // TODO - need to let the plane the transmission is aimed at that it's finished. + // However, for now we'll just release the frequency since if we don't it all goes pear-shaped + _releaseCounter = 0.0; + _releaseTime = 0.9; + _runReleaseCounter = true; + } + _counter += dt; + } +} + +void FGATC::ReceiveUserCallback(int code) { + SG_LOG(SG_ATC, SG_WARN, "WARNING - whichever ATC class was intended to receive callback code " << code << " didn't get it!!!"); +} + +void FGATC::SetResponseReqd(const string& rid) { + receiving = false; + responseReqd = true; + respond = false; // TODO - this ignores the fact that more than one plane could call this before response + // Shouldn't happen with AI only, but user could confuse things?? + responseID = rid; + runResponseCounter = true; + responseCounter = 0.0; + responseTime = 1.8; // TODO - randomize this slightly. +} + +void FGATC::NotifyTransmissionFinished(const string& rid) { + //cout << "Transmission finished, callsign = " << rid << '\n'; + receiving = false; + responseID = rid; + if(responseReqd) { + runResponseCounter = true; + responseCounter = 0.0; + responseTime = 1.2; // TODO - randomize this slightly, and allow it to be dependent on the transmission and how busy the ATC is. + respond = false; // TODO - this ignores the fact that more than one plane could call this before response + // Shouldn't happen with AI only, but user could confuse things?? + } else { + freqClear = true; + } +} + +void FGATC::Transmit(int callback_code) { + SG_LOG(SG_ATC, SG_INFO, "Transmit called by " << ident << " " << _type << ", msg = " << pending_transmission); + _pending = true; + _callback_code = callback_code; + _timeout = 0.0; +} + +void FGATC::ConditionalTransmit(double timeout, int callback_code) { + SG_LOG(SG_ATC, SG_INFO, "Timed transmit called by " << ident << " " << _type << ", msg = " << pending_transmission); + _pending = true; + _callback_code = callback_code; + _timeout = timeout; +} + +void FGATC::ImmediateTransmit(int callback_code) { + SG_LOG(SG_ATC, SG_INFO, "Immediate transmit called by " << ident << " " << _type << ", msg = " << pending_transmission); + if(_display) { + //Render(pending_transmission, ident, false); + Render(pending_transmission); + // At the moment Render doesn't work except for ATIS + } + if(callback_code) { + ProcessCallback(callback_code); + } +} + +// Derived classes should override this. +void FGATC::ProcessCallback(int code) { +} + +void FGATC::AddPlane(const string& pid) { +} + +int FGATC::RemovePlane() { + return 0; +} + +void FGATC::SetData(ATCData* d) { + lon = d->lon; + lat = d->lat; + elev = d->elev; + x = d->x; + y = d->y; + z = d->z; + range = d->range; + ident = d->ident; + name = d->name; + freq = d->freq; +} + +// Render a transmission +// Outputs the transmission either on screen or as audio depending on user preference +// The refname is a string to identify this sample to the sound manager +// The repeating flag indicates whether the message should be repeated continuously or played once. +void FGATC::Render(string& msg, const string& refname, bool repeating) { + if (repeating) + fgSetString("/sim/messages/atis", msg.c_str()); + else + fgSetString("/sim/messages/atc", msg.c_str()); + + #ifdef ENABLE_AUDIO_SUPPORT + _voice = (_voiceOK && fgGetBool("/sim/sound/voice")); + if(_voice) { + int len; + unsigned char* buf = _vPtr->WriteMessage((char*)msg.c_str(), len, _voice); + if(_voice) { + try { + SGSoundSample *simple + = new SGSoundSample(buf, len, 8000); + // TODO - at the moment the volume is always set off comm1 + // and can't be changed after the transmission has started. + simple->set_volume(5.0 * fgGetDouble("/instrumentation/comm[0]/volume")); + globals->get_soundmgr()->add(simple, refname); + if(repeating) { + globals->get_soundmgr()->play_looped(refname); + } else { + globals->get_soundmgr()->play_once(refname); + } + } catch ( sg_io_exception &e ) { + SG_LOG(SG_GENERAL, SG_ALERT, e.getFormattedMessage()); + } + } + delete[] buf; + } + #endif // ENABLE_AUDIO_SUPPORT + if(!_voice) { + // first rip the underscores and the pause hints out of the string - these are for the convienience of the voice parser + for(unsigned int i = 0; i < msg.length(); ++i) { + if((msg.substr(i,1) == "_") || (msg.substr(i,1) == "/")) { + msg[i] = ' '; + } + } + } + _playing = true; +} + + +// Cease rendering a transmission. +void FGATC::NoRender(const string& refname) { + if(_playing) { + if(_voice) { +#ifdef ENABLE_AUDIO_SUPPORT + globals->get_soundmgr()->stop(refname); + globals->get_soundmgr()->remove(refname); +#endif + } + _playing = false; + } +} + +// Generate the text of a message from its parameters and the current context. +string FGATC::GenText(const string& m, int c) { + return(""); +} + +ostream& operator << (ostream& os, atc_type atc) { + switch(atc) { + case(INVALID): return(os << "INVALID"); + case(ATIS): return(os << "ATIS"); + case(GROUND): return(os << "GROUND"); + case(TOWER): return(os << "TOWER"); + case(APPROACH): return(os << "APPROACH"); + case(DEPARTURE): return(os << "DEPARTURE"); + case(ENROUTE): return(os << "ENROUTE"); + } + return(os << "ERROR - Unknown switch in atc_type operator << "); +} diff --git a/src/ATCDCL/ATC.hxx b/src/ATCDCL/ATC.hxx new file mode 100644 index 000000000..0ea9d9c91 --- /dev/null +++ b/src/ATCDCL/ATC.hxx @@ -0,0 +1,308 @@ +// FGATC - abstract base class for the various actual atc classes +// such as FGATIS, FGTower etc. +// +// Written by David Luff, started Feburary 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_ATC_HXX +#define _FG_ATC_HXX + +#include <simgear/constants.h> +#include <simgear/compiler.h> +#include <simgear/misc/sgstream.hxx> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/debug/logstream.hxx> + +#include STL_IOSTREAM +#include STL_STRING + +#include "ATCVoice.hxx" + +SG_USING_STD(ostream); +SG_USING_STD(string); +SG_USING_STD(ios); + +enum plane_type { + UNKNOWN, + GA_SINGLE, + GA_HP_SINGLE, + GA_TWIN, + GA_JET, + MEDIUM, + HEAVY, + MIL_JET +}; + +// PlaneRec - a structure holding ATC-centric details of planes under control +// This might move or change eventually +struct PlaneRec { + plane_type type; + string callsign; + int squawkcode; +}; + +// Possible types of ATC type that the radios may be tuned to. +// INVALID implies not tuned in to anything. +enum atc_type { + INVALID, + ATIS, + GROUND, + TOWER, + APPROACH, + DEPARTURE, + ENROUTE +}; + +const int ATC_NUM_TYPES = 7; + +// DCL - new experimental ATC data store +struct ATCData { + atc_type type; + // I've deliberately used float instead of double here to keep the size down - we'll be storing thousands of these in memory. + // In fact, we could probably ditch x, y and z and generate on the fly as needed. + // On the other hand, we'll probably end up reading this data directly from the DAFIF eventually anyway!! + float lon, lat, elev; + float x, y, z; + //int freq; + unsigned short int freq; + //int range; + unsigned short int range; + string ident; + string name; +}; + +// perhaps we could use an FGRunway instead of this. +// That wouldn't cache the orthopos though. +struct RunwayDetails { + Point3D threshold_pos; + Point3D end1ortho; // ortho projection end1 (the threshold ATM) + Point3D end2ortho; // ortho projection end2 (the take off end in the current hardwired scheme) + double hdg; // true runway heading + double length; // In *METERS* + double width; // ditto + string rwyID; + int patternDirection; // -1 for left, 1 for right +}; + +ostream& operator << (ostream& os, atc_type atc); + +class FGATC { + +public: + + FGATC(); + virtual ~FGATC(); + + // Run the internal calculations + // Derived classes should call this method from their own Update methods if they + // wish to use the response timer functionality. + virtual void Update(double dt); + + // Recieve a coded callback from the ATC menu system based on the user's selection + virtual void ReceiveUserCallback(int code); + + // Add plane to a stack + virtual void AddPlane(const string& pid); + + // Remove plane from stack + virtual int RemovePlane(); + + // Indicate that this instance should output to the display if appropriate + inline void SetDisplay() { _display = true; } + + // Indicate that this instance should not output to the display + inline void SetNoDisplay() { _display = false; } + + // Generate the text of a message from its parameters and the current context. + virtual string GenText(const string& m, int c); + + // Returns true if OK to transmit on this frequency + inline bool GetFreqClear() { return freqClear; } + // Indicate that the frequency is in use + inline void SetFreqInUse() { freqClear = false; receiving = true; } + // Transmission to the ATC is finished and a response is required + void SetResponseReqd(const string& rid); + // Transmission finished - let ATC decide if a response is reqd and clear freq if necessary + void NotifyTransmissionFinished(const string& rid); + // Transmission finished and no response required + inline void ReleaseFreq() { freqClear = true; receiving = false; } // TODO - check that the plane releasing the freq is the right one etc. + // The above 3 funcs under development!! + // The idea is that AI traffic or the user ATC dialog box calls FreqInUse() when they begin transmitting, + // and that the tower control sets freqClear back to true following a reply. + // AI traffic should check FreqClear() is true prior to transmitting. + // The user will just have to wait for a gap in dialog as in real life. + + // Return the type of ATC station that the class represents + inline atc_type GetType() { return _type; } + + // Set the core ATC data + void SetData(ATCData* d); + + inline double get_lon() const { return lon; } + inline void set_lon(const double ln) {lon = ln;} + inline double get_lat() const { return lat; } + inline void set_lat(const double lt) {lat = lt;} + inline double get_elev() const { return elev; } + inline void set_elev(const double ev) {elev = ev;} + inline double get_x() const { return x; } + inline void set_x(const double ecs) {x = ecs;} + inline double get_y() const { return y; } + inline void set_y(const double why) {y = why;} + inline double get_z() const { return z; } + inline void set_z(const double zed) {z = zed;} + inline int get_freq() const { return freq; } + inline void set_freq(const int fq) {freq = fq;} + inline int get_range() const { return range; } + inline void set_range(const int rg) {range = rg;} + inline const string& get_ident() { return ident; } + inline void set_ident(const string& id) { ident = id; } + inline const string& get_name() { return name; } + inline void set_name(const string& nm) { name = nm; } + +protected: + + // Render a transmission + // Outputs the transmission either on screen or as audio depending on user preference + // The refname is a string to identify this sample to the sound manager + // The repeating flag indicates whether the message should be repeated continuously or played once. + void Render(string& msg, const string& refname = "", bool repeating = false); + + // Cease rendering all transmission from this station. + // Requires the sound manager refname if audio, else "". + void NoRender(const string& refname); + + // Transmit a message when channel becomes free of other dialog + void Transmit(int callback_code = 0); + + // Transmit a message if channel becomes free within timeout (seconds). timeout of zero implies no limit + void ConditionalTransmit(double timeout, int callback_code = 0); + + // Transmit regardless of other dialog on the channel eg emergency + void ImmediateTransmit(int callback_code = 0); + + virtual void ProcessCallback(int code); + + double lon, lat, elev; + double x, y, z; + int freq; + int range; + string ident; // Code of the airport its at. + string name; // Name transmitted in the broadcast. + atc_type _type; + + // Rendering related stuff + bool _voice; // Flag - true if we are using voice + bool _playing; // Indicates a message in progress + bool _voiceOK; // Flag - true if at least one voice has loaded OK + FGATCVoice* _vPtr; + + string pending_transmission; // derived classes set this string before calling Transmit(...) + bool freqClear; // Flag to indicate if the frequency is clear of ongoing dialog + bool receiving; // Flag to indicate we are receiving a transmission + bool responseReqd; // Flag to indicate we should be responding to a request/report + bool runResponseCounter; // Flag to indicate the response counter should be run + double responseTime; // Time to take from end of request transmission to beginning of response + // The idea is that this will be slightly random. + double responseCounter; // counter to implement the above + string responseID; // ID of the plane to respond to + bool respond; // Flag to indicate now is the time to respond - ie set following the count down of the response timer. + // Derived classes only need monitor this flag, and use the response ID, as long as they call FGATC::Update(...) + bool _runReleaseCounter; // A timer for releasing the frequency after giving the message enough time to display + double _releaseTime; + double _releaseCounter; + + bool _display; // Flag to indicate whether we should be outputting to the ATC display. + bool _displaying; // Flag to indicate whether we are outputting to the ATC display. + +private: + // Transmission timing stuff. + bool _pending; + double _timeout; + int _callback_code; // A callback code to be notified and processed by the derived classes + // A value of zero indicates no callback required + bool _transmit; // we are to transmit + bool _transmitting; // we are transmitting + double _counter; + double _max_count; +}; + +inline istream& +operator >> ( istream& fin, ATCData& a ) +{ + double f; + char ch; + char tp; + + fin >> tp; + + switch(tp) { + case 'I': + a.type = ATIS; + break; + case 'T': + a.type = TOWER; + break; + case 'G': + a.type = GROUND; + break; + case 'A': + a.type = APPROACH; + break; + case '[': + a.type = INVALID; + return fin >> skipeol; + default: + SG_LOG(SG_GENERAL, SG_ALERT, "Warning - unknown type \'" << tp << "\' found whilst reading ATC frequency data!\n"); + a.type = INVALID; + return fin >> skipeol; + } + + fin >> a.lat >> a.lon >> a.elev >> f >> a.range + >> a.ident; + + a.name = ""; + fin >> ch; + if(ch != '"') a.name += ch; + while(1) { + //in >> noskipws + fin.unsetf(ios::skipws); + fin >> ch; + if((ch == '"') || (ch == 0x0A)) { + break; + } // we shouldn't need the 0x0A but it makes a nice safely in case someone leaves off the " + a.name += ch; + } + fin.setf(ios::skipws); + //cout << "Comm name = " << a.name << '\n'; + + a.freq = (int)(f*100.0 + 0.5); + + // cout << a.ident << endl; + + // generate cartesian coordinates + Point3D geod( a.lon * SGD_DEGREES_TO_RADIANS, a.lat * SGD_DEGREES_TO_RADIANS, a.elev ); + Point3D cart = sgGeodToCart( geod ); + a.x = cart.x(); + a.y = cart.y(); + a.z = cart.z(); + + return fin >> skipeol; +} + + +#endif // _FG_ATC_HXX diff --git a/src/ATCDCL/ATCDialog.cxx b/src/ATCDCL/ATCDialog.cxx new file mode 100644 index 000000000..2946478da --- /dev/null +++ b/src/ATCDCL/ATCDialog.cxx @@ -0,0 +1,424 @@ +// ATCDialog.cxx - Functions and classes to handle the pop-up ATC dialog +// +// Written by Alexander Kappes and David Luff, started February 2003. +// +// Copyright (C) 2003 Alexander Kappes and David Luff +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include <simgear/compiler.h> + +#include <simgear/structure/commands.hxx> + +#include <Main/globals.hxx> +#include <GUI/gui.h> // mkDialog +#include <GUI/new_gui.hxx> + +#include "ATCDialog.hxx" +#include "ATC.hxx" +#include "ATCmgr.hxx" +#include "commlist.hxx" +#include "ATCutils.hxx" +#include <Airports/simple.hxx> + +#include <sstream> + +SG_USING_STD(ostringstream); + +FGATCDialog *current_atcdialog; + +// For the command manager - maybe eventually this should go in the built in command list +static bool do_ATC_dialog(const SGPropertyNode* arg) { + current_atcdialog->PopupDialog(); + return(true); +} + +static bool do_ATC_freq_search(const SGPropertyNode* arg) { + current_atcdialog->FreqDialog(); + return(true); +} + +ATCMenuEntry::ATCMenuEntry() { + stationid = ""; + //stationfr = 0; + transmission = ""; + menuentry = ""; + callback_code = 0; +} + +ATCMenuEntry::~ATCMenuEntry() { +} + +static void atcUppercase(string &s) { + for(unsigned int i=0; i<s.size(); ++i) { + s[i] = toupper(s[i]); + } +} + +// find child whose <name>...</name> entry matches 'name' +static SGPropertyNode *getNamedNode(SGPropertyNode *prop, const char *name) { + SGPropertyNode* p; + + for (int i = 0; i < prop->nChildren(); i++) + if ((p = getNamedNode(prop->getChild(i), name))) + return p; + + if (!strcmp(prop->getStringValue("name"), name)) + return prop; + + return 0; +} + + +FGATCDialog::FGATCDialog() { + _callbackPending = false; + _callbackTimer = 0.0; + _callbackWait = 0.0; + _callbackPtr = NULL; + _callbackCode = 0; + _gui = (NewGUI *)globals->get_subsystem("gui"); +} + +FGATCDialog::~FGATCDialog() { +} + +void FGATCDialog::Init() { + // Add ATC-dialog to the command list + globals->get_commands()->addCommand("ATC-dialog", do_ATC_dialog); + // Add ATC-freq-search to the command list + globals->get_commands()->addCommand("ATC-freq-search", do_ATC_freq_search); + + // initialize properties polled in Update() + globals->get_props()->setStringValue("/sim/atc/freq-airport", ""); + globals->get_props()->setIntValue("/sim/atc/transmission-num", -1); +} + +void FGATCDialog::Update(double dt) { + static SGPropertyNode_ptr airport = globals->get_props()->getNode("/sim/atc/freq-airport", true); + string s = airport->getStringValue(); + if (!s.empty()) { + airport->setStringValue(""); + FreqDisplay(s); + } + + static SGPropertyNode_ptr trans_num = globals->get_props()->getNode("/sim/atc/transmission-num", true); + int n = trans_num->getIntValue(); + if (n >= 0) { + trans_num->setIntValue(-1); + PopupCallback(n); + } + + if(_callbackPending) { + if(_callbackTimer > _callbackWait) { + _callbackPtr->ReceiveUserCallback(_callbackCode); + _callbackPtr->NotifyTransmissionFinished(fgGetString("/sim/user/callsign")); + _callbackPending = false; + } else { + _callbackTimer += dt; + } + } +} + +// Add an entry +void FGATCDialog::add_entry(const string& station, const string& transmission, const string& menutext, atc_type type, int code) { + + ATCMenuEntry a; + + a.stationid = station; + a.transmission = transmission; + a.menuentry = menutext; + a.callback_code = code; + + (available_dialog[type])[station.c_str()].push_back(a); + +} + +void FGATCDialog::remove_entry( const string &station, const string &trans, atc_type type ) { + atcmentry_vec_type* p = &((available_dialog[type])[station]); + atcmentry_vec_iterator current = p->begin(); + while(current != p->end()) { + if(current->transmission == trans) current = p->erase(current); + else ++current; + } +} + +void FGATCDialog::remove_entry( const string &station, int code, atc_type type ) { + atcmentry_vec_type* p = &((available_dialog[type])[station]); + atcmentry_vec_iterator current = p->begin(); + while(current != p->end()) { + if(current->callback_code == code) current = p->erase(current); + else ++current; + } +} + +// query the database whether the transmission is already registered; +bool FGATCDialog::trans_reg( const string &station, const string &trans, atc_type type ) { + atcmentry_vec_type* p = &((available_dialog[type])[station]); + atcmentry_vec_iterator current = p->begin(); + for ( ; current != p->end() ; ++current ) { + if ( current->transmission == trans ) return true; + } + return false; +} + +// query the database whether the transmission is already registered; +bool FGATCDialog::trans_reg( const string &station, int code, atc_type type ) { + atcmentry_vec_type* p = &((available_dialog[type])[station]); + atcmentry_vec_iterator current = p->begin(); + for ( ; current != p->end() ; ++current ) { + if ( current->callback_code == code ) return true; + } + return false; +} + +// Display the ATC popup dialog box with options relevant to the users current situation. +void FGATCDialog::PopupDialog() { + const char *dialog_name = "atc-dialog"; + SGPropertyNode_ptr dlg = _gui->getDialogProperties(dialog_name); + if (!dlg) + return; + + _gui->closeDialog(dialog_name); + + SGPropertyNode_ptr button_group = getNamedNode(dlg, "transmission-choice"); + // remove all transmission buttons + button_group->removeChildren("button", false); + + string label; + FGATC* atcptr = globals->get_ATC_mgr()->GetComm1ATCPointer(); // Hardwired to comm1 at the moment + + if (!atcptr) { + label = "Not currently tuned to any ATC service"; + mkDialog(label.c_str()); + return; + } + + if(atcptr->GetType() == ATIS) { + label = "Tuned to ATIS - no communication possible"; + mkDialog(label.c_str()); + return; + } + + atcmentry_vec_type atcmlist = (available_dialog[atcptr->GetType()])[atcptr->get_ident()]; + atcmentry_vec_iterator current = atcmlist.begin(); + atcmentry_vec_iterator last = atcmlist.end(); + + if(!atcmlist.size()) { + label = "No transmission available"; + mkDialog(label.c_str()); + return; + } + + const int bufsize = 32; + char buf[bufsize]; + // loop over all entries in atcmentrylist + for (int n = 0; n < 10; ++n) { + snprintf(buf, bufsize, "/sim/atc/opt[%d]", n); + fgSetBool(buf, false); + + if (current == last) + continue; + + // add transmission button (modified copy of <button-template>) + SGPropertyNode *entry = button_group->getNode("button", n, true); + copyProperties(button_group->getNode("button-template", true), entry); + entry->removeChildren("hide", false); + entry->setStringValue("property", buf); + entry->setIntValue("keynum", '1' + n); + if (n == 0) + entry->setBoolValue("default", true); + + snprintf(buf, bufsize, "%d", n + 1); + string legend = string(buf) + ". " + current->menuentry; + entry->setStringValue("legend", legend.c_str()); + entry->setIntValue("binding/value", n); + current++; + } + + _gui->showDialog(dialog_name); + return; +} + +void FGATCDialog::PopupCallback(int num) { + FGATC* atcptr = globals->get_ATC_mgr()->GetComm1ATCPointer(); // FIXME - Hardwired to comm1 at the moment + + if (!atcptr) + return; + + if (atcptr->GetType() == TOWER) { + //cout << "TOWER " << endl; + //cout << "ident is " << atcptr->get_ident() << endl; + atcmentry_vec_type atcmlist = (available_dialog[TOWER])[atcptr->get_ident()]; + unsigned int size = atcmlist.size(); + if(size && num < size) { + //cout << "Doing callback...\n"; + ATCMenuEntry a = atcmlist[num]; + atcptr->SetFreqInUse(); + string pilot = atcptr->GenText(a.transmission, a.callback_code); + fgSetString("/sim/messages/pilot", pilot.c_str()); + // This is the user's speech getting displayed. + _callbackPending = true; + _callbackTimer = 0.0; + _callbackWait = 5.0; + _callbackPtr = atcptr; + _callbackCode = a.callback_code; + } else { + //cout << "No options available...\n"; + } + //cout << "Donded" << endl; + } +} + +// map() key data type (removes duplicates and sorts by distance) +struct atcdata { + atcdata() {} + atcdata(const string i, const string n, const double d) { + id = i, name = n, distance = d; + } + bool operator<(const atcdata& a) const { + return id != a.id && distance < a.distance; + } + bool operator==(const atcdata& a) const { + return id == a.id && distance == a.distance; + } + string id; + string name; + double distance; +}; + +void FGATCDialog::FreqDialog() { + const char *dialog_name = "atc-freq-search"; + SGPropertyNode_ptr dlg = _gui->getDialogProperties(dialog_name); + if (!dlg) + return; + + _gui->closeDialog(dialog_name); + + SGPropertyNode_ptr button_group = getNamedNode(dlg, "quick-buttons"); + // remove all dynamic airport/ATC buttons + button_group->removeChildren("button", false); + + // Find the ATC stations within a reasonable range + comm_list_type atc_stations; + comm_list_iterator atc_stat_itr; + + double lon = fgGetDouble("/position/longitude-deg"); + double lat = fgGetDouble("/position/latitude-deg"); + double elev = fgGetDouble("/position/altitude-ft"); + Point3D aircraft = sgGeodToCart(Point3D(lon * SGD_DEGREES_TO_RADIANS, + lat * SGD_DEGREES_TO_RADIANS, elev)); + + // search stations in range + int num_stat = current_commlist->FindByPos(lon, lat, elev, 50.0, &atc_stations); + if (num_stat != 0) { + map<atcdata, bool> uniq; + // fill map (sorts by distance and removes duplicates) + comm_list_iterator itr = atc_stations.begin(); + for (; itr != atc_stations.end(); ++itr) { + Point3D station = Point3D(itr->x, itr->y, itr->z); + double distance = aircraft.distance3Dsquared(station); + uniq[atcdata(itr->ident, itr->name, distance)] = true; + } + // create button per map entry (modified copy of <button-template>) + map<atcdata, bool>::iterator uit = uniq.begin(); + for (int n = 0; uit != uniq.end() && n < 6; ++uit, ++n) { // max 6 buttons + SGPropertyNode *entry = button_group->getNode("button", n, true); + copyProperties(button_group->getNode("button-template", true), entry); + entry->removeChildren("hide", false); + entry->setStringValue("legend", uit->first.id.c_str()); + entry->setStringValue("binding[0]/value", uit->first.id.c_str()); + } + } + + // (un)hide message saying no things in range + SGPropertyNode_ptr range_error = getNamedNode(dlg, "no-atc-in-range"); + range_error->setBoolValue("hide", num_stat); + + _gui->showDialog(dialog_name); +} + +void FGATCDialog::FreqDisplay(string& ident) { + const char *dialog_name = "atc-freq-display"; + SGPropertyNode_ptr dlg = _gui->getDialogProperties(dialog_name); + if (!dlg) + return; + + _gui->closeDialog(dialog_name); + + SGPropertyNode_ptr freq_group = getNamedNode(dlg, "frequency-list"); + // remove all frequency entries + freq_group->removeChildren("group", false); + + atcUppercase(ident); + string label; + + const FGAirport *a = fgFindAirportID(ident); + if (!a) { + label = "Airport " + ident + " not found in database."; + mkDialog(label.c_str()); + return; + } + + // set title + label = ident + " Frequencies"; + dlg->setStringValue("text/label", label.c_str()); + + int n = 0; // Number of ATC frequencies at this airport + + comm_list_type stations; + int found = current_commlist->FindByPos(a->getLongitude(), a->getLatitude(), a->getElevation(), 20.0, &stations); + if(found) { + ostringstream ostr; + comm_list_iterator itr = stations.begin(); + for (n = 0; itr != stations.end(); ++itr) { + if(itr->ident != ident) + continue; + + if(itr->type == INVALID) + continue; + + // add frequency line (modified copy of <group-template>) + SGPropertyNode *entry = freq_group->getNode("group", n, true); + copyProperties(freq_group->getNode("group-template", true), entry); + entry->removeChildren("hide", false); + + ostr << itr->type; + entry->setStringValue("text[0]/label", ostr.str().c_str()); + + char buf[8]; + snprintf(buf, 8, "%.2f", (itr->freq / 100.0)); // Convert from KHz to MHz + if(buf[5] == '3') buf[5] = '2'; + if(buf[5] == '8') buf[5] = '7'; + buf[7] = '\0'; + + entry->setStringValue("text[1]/label", buf); + + ostr.seekp(0); + n++; + } + } + if(n == 0) { + label = "No frequencies found for airport " + ident; + mkDialog(label.c_str()); + return; + } + + _gui->showDialog(dialog_name); +} + diff --git a/src/ATCDCL/ATCDialog.hxx b/src/ATCDCL/ATCDialog.hxx new file mode 100644 index 000000000..76cf2cc9f --- /dev/null +++ b/src/ATCDCL/ATCDialog.hxx @@ -0,0 +1,115 @@ +// ATCDialog.hxx - Functions and classes to handle the pop-up ATC dialog +// +// Written by Alexander Kappes and David Luff, started February 2003. +// +// Copyright (C) 2003 Alexander Kappes and David Luff +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef ATC_DIALOG_HXX +#define ATC_DIALOG_HXX + +#include <simgear/compiler.h> + +#include <vector> +#include <map> + +#include "ATC.hxx" + +SG_USING_STD(vector); +SG_USING_STD(map); + +class NewGUI; + +// ATCMenuEntry - an encapsulation of an entry in the ATC dialog +struct ATCMenuEntry { + + string stationid; // ID of transmitting station + //int stationfr; // ? + string transmission; // Actual speech of transmission + string menuentry; // Shortened version for display in the dialog + int callback_code; // This code is supplied by the registering station, and then + // returned to the registering station if that option is chosen. + // The actual value is only understood by the registering station - + // FGATCDialog only stores it and returns it if appropriate. + + ATCMenuEntry(); + ~ATCMenuEntry(); +}; + +typedef vector < ATCMenuEntry > atcmentry_vec_type; +typedef atcmentry_vec_type::iterator atcmentry_vec_iterator; + +typedef map < string, atcmentry_vec_type > atcmentry_map_type; +typedef atcmentry_map_type::iterator atcmentry_map_iterator; + +//void ATCDialogInit(); + +//void ATCDoDialog(atc_type type); + +class FGATCDialog { + +public: + + FGATCDialog(); + ~FGATCDialog(); + + void Init(); + + void Update(double dt); + + void PopupDialog(); + + void PopupCallback(int); + + void add_entry( const string& station, const string& transmission, const string& menutext, atc_type type, int code); + + void remove_entry( const string &station, const string &trans, atc_type type ); + + void remove_entry( const string &station, int code, atc_type type ); + + // query the database whether the transmission is already registered; + bool trans_reg( const string &station, const string &trans, atc_type type ); + + // query the database whether the transmission is already registered; + bool trans_reg( const string &station, int code, atc_type type ); + + // Display a frequency search dialog for nearby stations + void FreqDialog(); + + // Display the comm ATC frequencies for airport ident + // where ident is a valid ICAO code. + void FreqDisplay(string& ident); + +private: + + atcmentry_map_type available_dialog[ATC_NUM_TYPES]; + + int freq; + bool reset; + + bool _callbackPending; + double _callbackTimer; + double _callbackWait; + FGATC* _callbackPtr; + int _callbackCode; + + NewGUI *_gui; +}; + +extern FGATCDialog *current_atcdialog; + +#endif // ATC_DIALOG_HXX + diff --git a/src/ATCDCL/ATCProjection.cxx b/src/ATCDCL/ATCProjection.cxx new file mode 100644 index 000000000..7028004c8 --- /dev/null +++ b/src/ATCDCL/ATCProjection.cxx @@ -0,0 +1,118 @@ +// ATCProjection.cxx - A convienience projection class for the ATC/AI system. +// +// Written by David Luff, started 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "ATCProjection.hxx" +#include <math.h> +#include <simgear/constants.h> + +FGATCProjection::FGATCProjection() { + _origin.setlat(0.0); + _origin.setlon(0.0); + _origin.setelev(0.0); + _correction_factor = cos(_origin.lat() * SG_DEGREES_TO_RADIANS); +} + +FGATCProjection::FGATCProjection(const Point3D& centre) { + _origin = centre; + _correction_factor = cos(_origin.lat() * SG_DEGREES_TO_RADIANS); +} + +FGATCProjection::~FGATCProjection() { +} + +void FGATCProjection::Init(const Point3D& centre) { + _origin = centre; + _correction_factor = cos(_origin.lat() * SG_DEGREES_TO_RADIANS); +} + +Point3D FGATCProjection::ConvertToLocal(const Point3D& pt) { + double delta_lat = pt.lat() - _origin.lat(); + double delta_lon = pt.lon() - _origin.lon(); + + double y = sin(delta_lat * SG_DEGREES_TO_RADIANS) * SG_EQUATORIAL_RADIUS_M; + double x = sin(delta_lon * SG_DEGREES_TO_RADIANS) * SG_EQUATORIAL_RADIUS_M * _correction_factor; + + return(Point3D(x,y,0.0)); +} + +Point3D FGATCProjection::ConvertFromLocal(const Point3D& pt) { + double delta_lat = asin(pt.y() / SG_EQUATORIAL_RADIUS_M) * SG_RADIANS_TO_DEGREES; + double delta_lon = (asin(pt.x() / SG_EQUATORIAL_RADIUS_M) * SG_RADIANS_TO_DEGREES) / _correction_factor; + + return(Point3D(_origin.lon()+delta_lon, _origin.lat()+delta_lat, 0.0)); +} + +/**********************************************************************************/ + +FGATCAlignedProjection::FGATCAlignedProjection() { + _origin.setlat(0.0); + _origin.setlon(0.0); + _origin.setelev(0.0); + _correction_factor = cos(_origin.lat() * SG_DEGREES_TO_RADIANS); +} + +FGATCAlignedProjection::FGATCAlignedProjection(const Point3D& centre, double heading) { + _origin = centre; + _theta = heading * SG_DEGREES_TO_RADIANS; + _correction_factor = cos(_origin.lat() * SG_DEGREES_TO_RADIANS); +} + +FGATCAlignedProjection::~FGATCAlignedProjection() { +} + +void FGATCAlignedProjection::Init(const Point3D& centre, double heading) { + _origin = centre; + _theta = heading * SG_DEGREES_TO_RADIANS; + _correction_factor = cos(_origin.lat() * SG_DEGREES_TO_RADIANS); +} + +Point3D FGATCAlignedProjection::ConvertToLocal(const Point3D& pt) { + // convert from lat/lon to orthogonal + double delta_lat = pt.lat() - _origin.lat(); + double delta_lon = pt.lon() - _origin.lon(); + double y = sin(delta_lat * SG_DEGREES_TO_RADIANS) * SG_EQUATORIAL_RADIUS_M; + double x = sin(delta_lon * SG_DEGREES_TO_RADIANS) * SG_EQUATORIAL_RADIUS_M * _correction_factor; + + // Align + if(_theta != 0.0) { + double xbar = x; + x = x*cos(_theta) - y*sin(_theta); + y = (xbar*sin(_theta)) + (y*cos(_theta)); + } + + return(Point3D(x,y,pt.elev())); +} + +Point3D FGATCAlignedProjection::ConvertFromLocal(const Point3D& pt) { + // de-align + double thi = _theta * -1.0; + double x = pt.x()*cos(thi) - pt.y()*sin(thi); + double y = (pt.x()*sin(thi)) + (pt.y()*cos(thi)); + + // convert from orthogonal to lat/lon + double delta_lat = asin(y / SG_EQUATORIAL_RADIUS_M) * SG_RADIANS_TO_DEGREES; + double delta_lon = (asin(x / SG_EQUATORIAL_RADIUS_M) * SG_RADIANS_TO_DEGREES) / _correction_factor; + + return(Point3D(_origin.lon()+delta_lon, _origin.lat()+delta_lat, pt.elev())); +} diff --git a/src/ATCDCL/ATCProjection.hxx b/src/ATCDCL/ATCProjection.hxx new file mode 100644 index 000000000..faf8eb395 --- /dev/null +++ b/src/ATCDCL/ATCProjection.hxx @@ -0,0 +1,73 @@ +// ATCProjection.hxx - A convienience projection class for the ATC/AI system. +// +// Written by David Luff, started 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_ATC_PROJECTION_HXX +#define _FG_ATC_PROJECTION_HXX + +#include <simgear/math/point3d.hxx> + +// FGATCProjection - a class to project an area local to an airport onto an orthogonal co-ordinate system +class FGATCProjection { + +public: + FGATCProjection(); + FGATCProjection(const Point3D& centre); + ~FGATCProjection(); + + void Init(const Point3D& centre); + + // Convert a lat/lon co-ordinate (degrees) to the local projection (meters) + Point3D ConvertToLocal(const Point3D& pt); + + // Convert a local projection co-ordinate (meters) to lat/lon (degrees) + Point3D ConvertFromLocal(const Point3D& pt); + +private: + Point3D _origin; // lat/lon of local area origin + double _correction_factor; // Reduction in surface distance per degree of longitude due to latitude. Saves having to do a cos() every call. + +}; + + +// FGATCAlignedProjection - a class to project an area local to a runway onto an orthogonal co-ordinate system +// with the origin at the threshold and the runway aligned with the y axis. +class FGATCAlignedProjection { + +public: + FGATCAlignedProjection(); + FGATCAlignedProjection(const Point3D& centre, double heading); + ~FGATCAlignedProjection(); + + void Init(const Point3D& centre, double heading); + + // Convert a lat/lon co-ordinate (degrees) to the local projection (meters) + Point3D ConvertToLocal(const Point3D& pt); + + // Convert a local projection co-ordinate (meters) to lat/lon (degrees) + Point3D ConvertFromLocal(const Point3D& pt); + +private: + Point3D _origin; // lat/lon of local area origin (the threshold) + double _theta; // the rotation angle for alignment in radians + double _correction_factor; // Reduction in surface distance per degree of longitude due to latitude. Saves having to do a cos() every call. + +}; + +#endif // _FG_ATC_PROJECTION_HXX diff --git a/src/ATCDCL/ATCVoice.cxx b/src/ATCDCL/ATCVoice.cxx new file mode 100644 index 000000000..682cd24a4 --- /dev/null +++ b/src/ATCDCL/ATCVoice.cxx @@ -0,0 +1,191 @@ +// FGATCVoice.cxx - a class to encapsulate an ATC voice +// +// Written by David Luff, started November 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/misc/sg_path.hxx> +#include <simgear/debug/logstream.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/math/sg_random.h> +#include <Main/globals.hxx> + +#include "ATCVoice.hxx" + +#include <stdlib.h> + +FGATCVoice::FGATCVoice() { + SoundData = 0; + rawSoundData = 0; +} + +FGATCVoice::~FGATCVoice() { + if (rawSoundData) + free( rawSoundData ); + delete SoundData; +} + +// Load the two voice files - one containing the raw sound data (.wav) and one containing the word positions (.vce). +// Return true if successful. +bool FGATCVoice::LoadVoice(const string& voice) { + // FIXME CLO: disabled to try to see if this is causign problemcs + // return false; + + ifstream fin; + + SGPath path = globals->get_fg_root(); + path.append( "ATC" ); + + string file = voice + ".wav"; + + SoundData = new SGSoundSample(); + rawSoundData = (char *)SoundData->load_file(path.c_str(), file.c_str()); + rawDataSize = SoundData->get_size(); + + path = globals->get_fg_root(); + string wordPath = "ATC/" + voice + ".vce"; + path.append(wordPath); + + // Now load the word data + fin.open(path.c_str(), ios::in); + if(!fin) { + SG_LOG(SG_ATC, SG_ALERT, "Unable to open input file " << path.c_str()); + return(false); + } + SG_LOG(SG_ATC, SG_INFO, "Opened word data file " << wordPath << " OK..."); + char numwds[10]; + char wrd[100]; + string wrdstr; + char wrdOffsetStr[20]; + char wrdLengthStr[20]; + unsigned int wrdOffset; // Offset into the raw sound data that the word sample begins + unsigned int wrdLength; // Length of the word sample in bytes + WordData wd; + fin >> numwds; + unsigned int numwords = atoi(numwds); + //cout << numwords << '\n'; + for(unsigned int i=0; i < numwords; ++i) { + fin >> wrd; + wrdstr = wrd; + fin >> wrdOffsetStr; + fin >> wrdLengthStr; + wrdOffset = atoi(wrdOffsetStr); + wrdLength = atoi(wrdLengthStr); + wd.offset = wrdOffset; + wd.length = wrdLength; + wordMap[wrdstr] = wd; + //cout << wrd << "\t\t" << wrdOffset << "\t\t" << wrdLength << '\n'; + //cout << i << '\n'; + } + + fin.close(); + return(true); +} + + +typedef list < string > tokenList_type; +typedef tokenList_type::iterator tokenList_iterator; + +// Given a desired message, return a pointer to the data buffer and write the buffer length into len. +unsigned char* FGATCVoice::WriteMessage(char* message, int& len, bool& dataOK) { + + // What should we do here? + // First - parse the message into a list of tokens. + // Sort the tokens into those we understand and those we don't. + // Add all the raw lengths of the token sound data, allocate enough space, and fill it with the rqd data. + tokenList_type tokenList; + tokenList_iterator tokenListItr; + + // TODO - at the moment we're effectively taking 3 passes through the data. + // There is no need for this - 2 should be sufficient - we can probably ditch the tokenList. + char* token; + char mes[1000]; + int numWords = 0; + strcpy(mes, message); + const char delimiters[] = " \t.,;:\""; + token = strtok(mes, delimiters); + while(token != NULL) { + tokenList.push_back(token); + ++numWords; + //cout << "token = " << token << '\n'; + token = strtok(NULL, delimiters); + } + + WordData* wdptr = new WordData[numWords]; + int word = 0; + unsigned int cumLength = 0; + + tokenListItr = tokenList.begin(); + while(tokenListItr != tokenList.end()) { + if(wordMap.find(*tokenListItr) == wordMap.end()) { + // Oh dear - the token isn't in the sound file + //cout << "word " << *tokenListItr << " not found :-(\n"; + } else { + wdptr[word] = wordMap[*tokenListItr]; + cumLength += wdptr[word].length; + //cout << *tokenListItr << " found at offset " << wdptr[word].offset << " with length " << wdptr[word].length << endl; + word++; + } + ++tokenListItr; + } + + // Check for no tokens found else slScheduler can be crashed + if(!word) { + dataOK = false; + return(NULL); + } + + unsigned char* tmpbuf = new unsigned char[cumLength]; + unsigned char* outbuf = new unsigned char[cumLength]; + len = cumLength; + unsigned int bufpos = 0; + for(int i=0; i<word; ++i) { + /* + * Sanity check for corrupt/mismatched sound data input - avoids a seg fault + * (As long as the calling function checks the return value!!) + * This check should be left in even when the default Flightgear files are known + * to be OK since it checks for mis-indexing of voice files by 3rd party developers. + */ + if((wdptr[i].offset + wdptr[i].length) > rawDataSize) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR - mismatch between ATC .wav and .vce file in ATCVoice.cxx\n"); + SG_LOG(SG_ATC, SG_ALERT, "Offset + length: " << wdptr[i].offset + wdptr[i].length + << " exceeds rawdata size: " << rawDataSize << endl); + delete[] wdptr; + dataOK = false; + return(NULL); + } + memcpy(tmpbuf + bufpos, rawSoundData + wdptr[i].offset, wdptr[i].length); + bufpos += wdptr[i].length; + } + + // tmpbuf now contains the message starting at the beginning - but we want it to start at a random position. + unsigned int offsetIn = (int)(cumLength * sg_random()); + if(offsetIn > cumLength) offsetIn = cumLength; + memcpy(outbuf, tmpbuf + offsetIn, (cumLength - offsetIn)); + memcpy(outbuf + (cumLength - offsetIn), tmpbuf, offsetIn); + + delete[] tmpbuf; + delete[] wdptr; + + dataOK = true; + return(outbuf); +} diff --git a/src/ATCDCL/ATCVoice.hxx b/src/ATCDCL/ATCVoice.hxx new file mode 100644 index 000000000..a5ea59394 --- /dev/null +++ b/src/ATCDCL/ATCVoice.hxx @@ -0,0 +1,87 @@ +// FGATCVoice.hxx - a class to encapsulate an ATC voice +// +// Written by David Luff, started November 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_ATC_VOICE +#define _FG_ATC_VOICE + +#include <simgear/compiler.h> + +#if defined( SG_HAVE_STD_INCLUDES ) || defined( __BORLANDC__ ) || (__APPLE__) +# include <fstream> +# include <iostream> +#else +# include <fstream.h> +# include <iostream.h> +#endif + +#include <map> +#include <list> +#include <string> + +#include <simgear/sound/sample_openal.hxx> + +SG_USING_STD(map); +SG_USING_STD(list); +SG_USING_STD(string); + +SG_USING_STD(cout); +SG_USING_STD(ios); +SG_USING_STD(ofstream); +SG_USING_STD(ifstream); + + +struct WordData { + unsigned int offset; // Offset of beginning of word sample into raw sound sample + unsigned int length; // Byte length of word sample +}; + +typedef map < string, WordData > atc_word_map_type; +typedef atc_word_map_type::iterator atc_word_map_iterator; +typedef atc_word_map_type::const_iterator atc_word_map_const_iterator; + +class FGATCVoice { + +public: + + FGATCVoice(); + ~FGATCVoice(); + + // Load the two voice files - one containing the raw sound data (.wav) and one containing the word positions (.vce). + // Return true if successful. + bool LoadVoice(const string& voice); + + // Given a desired message, return a pointer to the data buffer and write the buffer length into len. + // Sets dataOK = true if the returned buffer is valid. + unsigned char* WriteMessage(char* message, int& len, bool& dataOK); + + +private: + + // the sound and word position data + char* rawSoundData; + unsigned int rawDataSize; + SGSoundSample *SoundData; + + // A map of words vs. byte position and length in rawSoundData + atc_word_map_type wordMap; + +}; + +#endif // _FG_ATC_VOICE diff --git a/src/ATCDCL/ATCmgr.cxx b/src/ATCDCL/ATCmgr.cxx new file mode 100644 index 000000000..bbf99dfa6 --- /dev/null +++ b/src/ATCDCL/ATCmgr.cxx @@ -0,0 +1,723 @@ +// ATCmgr.cxx - Implementation of FGATCMgr - a global Flightgear ATC manager. +// +// Written by David Luff, started February 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/misc/sg_path.hxx> +#include <simgear/debug/logstream.hxx> +#include <Airports/simple.hxx> + +#include "ATCmgr.hxx" +#include "commlist.hxx" +#include "ATCDialog.hxx" +#include "ATCutils.hxx" +#include "transmissionlist.hxx" +#include "ground.hxx" + + +/* +// periodic radio station search wrapper +static void fgATCSearch( void ) { + globals->get_ATC_mgr()->Search(); +} +*/ //This wouldn't compile - including Time/event.hxx breaks it :-( + // Is this still true?? -EMH- + +AirportATC::AirportATC() : + lon(0.0), + lat(0.0), + elev(0.0), + atis_freq(0.0), + atis_active(false), + tower_freq(0.0), + tower_active(false), + ground_freq(0.0), + ground_active(false), + set_by_AI(false), + numAI(0) + //airport_atc_map.clear(); +{ + for(int i=0; i<ATC_NUM_TYPES; ++i) { + set_by_comm[0][i] = false; + set_by_comm[1][i] = false; + } +} + +FGATCMgr::FGATCMgr() { + comm_ident[0] = ""; + comm_ident[1] = ""; + //last_comm_ident[0] = ""; + //last_comm_ident[1] = ""; + //approach_ident = ""; + last_in_range = false; + comm_type[0] = INVALID; + comm_type[1] = INVALID; + comm_atc_ptr[0] = NULL; + comm_atc_ptr[1] = NULL; + comm_valid[0] = false; + comm_valid[1] = false; + + initDone = false; +} + +FGATCMgr::~FGATCMgr() { + delete v1; +} + +void FGATCMgr::bind() { +} + +void FGATCMgr::unbind() { +} + +void FGATCMgr::init() { + //cout << "ATCMgr::init called..." << endl; + + comm_node[0] = fgGetNode("/instrumentation/comm[0]/frequencies/selected-mhz", true); + comm_node[1] = fgGetNode("/instrumentation/comm[1]/frequencies/selected-mhz", true); + lon_node = fgGetNode("/position/longitude-deg", true); + lat_node = fgGetNode("/position/latitude-deg", true); + elev_node = fgGetNode("/position/altitude-ft", true); + atc_list_itr = atc_list.begin(); + + // Search for connected ATC stations once per 0.8 seconds or so + // globals->get_event_mgr()->add( "fgATCSearch()", fgATCSearch, + // FGEvent::FG_EVENT_READY, 800); + // + // For some reason the above doesn't compile - including Time/event.hxx stops compilation. + // Is this still true after the reorganization of the event managar?? + // -EMH- + + // Initialise the frequency search map + current_commlist = new FGCommList; + SGPath p_comm( globals->get_fg_root() ); + current_commlist->init( p_comm ); + +#ifdef ENABLE_AUDIO_SUPPORT + // Load all available voices. + // For now we'll do one hardwired one + + v1 = new FGATCVoice; + try { + voiceOK = v1->LoadVoice("default"); + voice = true; + } catch ( sg_io_exception & ) { + voiceOK = false; + voice = false; + delete v1; + v1 = 0; + } + + /* I've loaded the voice even if /sim/sound/pause is true + * since I know no way of forcing load of the voice if the user + * subsequently switches /sim/sound/audible to true. + * (which is the right thing to do -- CLO) :-) */ +#else + voice = false; +#endif + + // Initialise the ATC Dialogs + //cout << "Initing Transmissions..." << endl; + SG_LOG(SG_ATC, SG_INFO, " ATC Transmissions"); + current_transmissionlist = new FGTransmissionList; + SGPath p_transmission( globals->get_fg_root() ); + p_transmission.append( "ATC/default.transmissions" ); + current_transmissionlist->init( p_transmission ); + //cout << "Done Transmissions" << endl; + + SG_LOG(SG_ATC, SG_INFO, " ATC Dialog System"); + current_atcdialog = new FGATCDialog; + current_atcdialog->Init(); + + initDone = true; + //cout << "ATCmgr::init done!" << endl; +} + +void FGATCMgr::update(double dt) { + if(!initDone) { + init(); + SG_LOG(SG_ATC, SG_WARN, "Warning - ATCMgr::update(...) called before ATCMgr::init()"); + } + + current_atcdialog->Update(dt); + + //cout << "Entering update..." << endl; + //Traverse the list of active stations. + //Only update one class per update step to avoid the whole ATC system having to calculate between frames. + //Eventually we should only update every so many steps. + //cout << "In FGATCMgr::update - atc_list.size = " << atc_list.size() << endl; + if(atc_list.size()) { + if(atc_list_itr == atc_list.end()) { + atc_list_itr = atc_list.begin(); + } + //cout << "Updating " << (*atc_list_itr)->get_ident() << ' ' << (*atc_list_itr)->GetType() << '\n'; + //cout << "Freq = " << (*atc_list_itr)->get_freq() << '\n'; + (*atc_list_itr)->Update(dt * atc_list.size()); + //cout << "Done ATC update..." << endl; + ++atc_list_itr; + } + + /* + cout << "ATC_LIST: " << atc_list.size() << ' '; + for(atc_list_iterator it = atc_list.begin(); it != atc_list.end(); it++) { + cout << (*it)->get_ident() << ' '; + } + cout << '\n'; + */ + + // Search the tuned frequencies every now and then - this should be done with the event scheduler + static int i = 0; // Very ugly - but there should only ever be one instance of FGATCMgr. + /* + if(i == 7) { + //cout << "About to AreaSearch()" << endl; + AreaSearch(); + } + */ + if(i == 15) { + //cout << "About to search(1)" << endl; + FreqSearch(1); + } + if(i == 30) { + //cout << "About to search(2)" << endl; + FreqSearch(2); + i = 0; + } + ++i; + + //cout << "comm1 type = " << comm_type[0] << '\n'; + //cout << "Leaving update..." << endl; +} + + +// Returns frequency in KHz - should I alter this to return in MHz? +unsigned short int FGATCMgr::GetFrequency(const string& ident, const atc_type& tp) { + ATCData test; + bool ok = current_commlist->FindByCode(ident, test, tp); + return(ok ? test.freq : 0); +} + + +// Register the fact that the AI system wants to activate an airport +// Might need more sophistication in this in the future - eg registration by aircraft call-sign. +bool FGATCMgr::AIRegisterAirport(const string& ident) { + SG_LOG(SG_ATC, SG_BULK, "AI registered airport " << ident << " with the ATC system"); + //cout << "AI registered airport " << ident << " with the ATC system" << '\n'; + if(airport_atc_map.find(ident) != airport_atc_map.end()) { + airport_atc_map[ident]->set_by_AI = true; + airport_atc_map[ident]->numAI++; + return(true); + } else { + const FGAirport *ap = fgFindAirportID(ident); + if (ap) { + //cout << "ident = " << ident << '\n'; + AirportATC *a = new AirportATC; + // I'm not entirely sure that this AirportATC structure business is actually needed - it just duplicates what we can find out anyway! + a->lon = ap->getLongitude(); + a->lat = ap->getLatitude(); + a->elev = ap->getElevation(); + a->atis_freq = GetFrequency(ident, ATIS); + //cout << "ATIS freq = " << a->atis_freq << '\n'; + a->atis_active = false; + a->tower_freq = GetFrequency(ident, TOWER); + //cout << "Tower freq = " << a->tower_freq << '\n'; + a->tower_active = false; + a->ground_freq = GetFrequency(ident, GROUND); + //cout << "Ground freq = " << a->ground_freq << '\n'; + a->ground_active = false; + // TODO - some airports will have a tower/ground frequency but be inactive overnight. + a->set_by_AI = true; + a->numAI = 1; + airport_atc_map[ident] = a; + return(true); + } else { + SG_LOG(SG_ATC, SG_ALERT, "ERROR - can't find airport " << ident << " in AIRegisterAirport(...)"); + } + } + return(false); +} + + +// Register the fact that the comm radio is tuned to an airport +// Channel is zero based +bool FGATCMgr::CommRegisterAirport(const string& ident, int chan, const atc_type& tp) { + SG_LOG(SG_ATC, SG_BULK, "Comm channel " << chan << " registered airport " << ident); + //cout << "Comm channel " << chan << " registered airport " << ident << ' ' << tp << '\n'; + if(airport_atc_map.find(ident) != airport_atc_map.end()) { + //cout << "IN MAP - flagging set by comm..." << endl; + airport_atc_map[ident]->set_by_comm[chan][tp] = true; + if(tp == ATIS) { + airport_atc_map[ident]->atis_active = true; + } else if(tp == TOWER) { + airport_atc_map[ident]->tower_active = true; + } else if(tp == GROUND) { + airport_atc_map[ident]->ground_active = true; + } else if(tp == APPROACH) { + //a->approach_active = true; + } // TODO - there *must* be a better way to do this!!! + return(true); + } else { + //cout << "NOT IN MAP - creating new..." << endl; + const FGAirport *ap = fgFindAirportID(ident); + if (ap) { + AirportATC *a = new AirportATC; + // I'm not entirely sure that this AirportATC structure business is actually needed - it just duplicates what we can find out anyway! + a->lon = ap->getLongitude(); + a->lat = ap->getLatitude(); + a->elev = ap->getElevation(); + a->atis_freq = GetFrequency(ident, ATIS); + a->atis_active = false; + a->tower_freq = GetFrequency(ident, TOWER); + a->tower_active = false; + a->ground_freq = GetFrequency(ident, GROUND); + a->ground_active = false; + if(tp == ATIS) { + a->atis_active = true; + } else if(tp == TOWER) { + a->tower_active = true; + } else if(tp == GROUND) { + a->ground_active = true; + } else if(tp == APPROACH) { + //a->approach_active = true; + } // TODO - there *must* be a better way to do this!!! + // TODO - some airports will have a tower/ground frequency but be inactive overnight. + a->set_by_AI = false; + a->numAI = 0; + a->set_by_comm[chan][tp] = true; + airport_atc_map[ident] = a; + return(true); + } + } + return(false); +} + + +// Remove from list only if not needed by the AI system or the other comm channel +// Note that chan is zero based. +void FGATCMgr::CommRemoveFromList(const string& id, const atc_type& tp, int chan) { + SG_LOG(SG_ATC, SG_BULK, "CommRemoveFromList called for airport " << id << " " << tp << " by channel " << chan); + //cout << "CommRemoveFromList called for airport " << id << " " << tp << " by channel " << chan << '\n'; + if(airport_atc_map.find(id) != airport_atc_map.end()) { + AirportATC* a = airport_atc_map[id]; + //cout << "In CommRemoveFromList, a->ground_freq = " << a->ground_freq << endl; + if(a->set_by_AI && tp != ATIS) { + // Set by AI, so don't remove simply because user isn't tuned in any more - just stop displaying + SG_LOG(SG_ATC, SG_BULK, "In CommRemoveFromList, service was set by AI\n"); + FGATC* aptr = GetATCPointer(id, tp); + switch(chan) { + case 0: + //cout << "chan 1\n"; + a->set_by_comm[0][tp] = false; + if(!a->set_by_comm[1][tp]) { + //cout << "not set by comm2\n"; + if(aptr != NULL) { + //cout << "Got pointer\n"; + aptr->SetNoDisplay(); + //cout << "Setting no display...\n"; + } else { + //cout << "Not got pointer\n"; + } + } + break; + case 1: + a->set_by_comm[1][tp] = false; + if(!a->set_by_comm[0][tp]) { + if(aptr != NULL) { + aptr->SetNoDisplay(); + //cout << "Setting no display...\n"; + } + } + break; + } + //airport_atc_map[id] = a; + return; + } else { + switch(chan) { + case 0: + a->set_by_comm[0][tp] = false; + // Remove only if not also set by the other comm channel + if(!a->set_by_comm[1][tp]) { + a->tower_active = false; + a->ground_active = false; + RemoveFromList(id, tp); + } + break; + case 1: + a->set_by_comm[1][tp] = false; + if(!a->set_by_comm[0][tp]) { + a->tower_active = false; + a->ground_active = false; + RemoveFromList(id, tp); + } + break; + } + } + } +} + + +// Remove from list - should only be called from above or similar +// This function *will* remove it from the list regardless of who else might want it. +void FGATCMgr::RemoveFromList(const string& id, const atc_type& tp) { + //cout << "FGATCMgr::RemoveFromList called..." << endl; + //cout << "Requested type = " << tp << endl; + //cout << "id = " << id << endl; + atc_list_iterator it = atc_list.begin(); + while(it != atc_list.end()) { + //cout << "type = " << (*it)->GetType() << '\n'; + //cout << "Ident = " << (*it)->get_ident() << '\n'; + if( ((*it)->get_ident() == id) + && ((*it)->GetType() == tp) ) { + //Before removing it stop it transmitting!! + //cout << "OBLITERATING FROM LIST!!!\n"; + (*it)->SetNoDisplay(); + (*it)->Update(0.00833); + delete (*it); + atc_list.erase(it); + atc_list_itr = atc_list.begin(); // Reset the persistent itr incase we've left it off the end. + break; + } + ++it; + } +} + + +// Find in list - return a currently active ATC pointer given ICAO code and type +// Return NULL if the given service is not in the list +// - *** THE CALLING FUNCTION MUST CHECK FOR THIS *** +FGATC* FGATCMgr::FindInList(const string& id, const atc_type& tp) { + //cout << "Entering FindInList for " << id << ' ' << tp << endl; + atc_list_iterator it = atc_list.begin(); + while(it != atc_list.end()) { + if( ((*it)->get_ident() == id) + && ((*it)->GetType() == tp) ) { + return(*it); + } + ++it; + } + // If we get here it's not in the list + //cout << "Couldn't find it in the list though :-(" << endl; + return(NULL); +} + +// Returns true if the airport is found in the map +bool FGATCMgr::GetAirportATCDetails(const string& icao, AirportATC* a) { + if(airport_atc_map.find(icao) != airport_atc_map.end()) { + *a = *airport_atc_map[icao]; + return(true); + } else { + return(false); + } +} + + +// Return a pointer to a given sort of ATC at a given airport and activate if necessary +// Returns NULL if service doesn't exist - calling function should check for this. +// We really ought to make this private and call it from the CommRegisterAirport / AIRegisterAirport functions +// - at the moment all these GetATC... functions exposed are just too complicated. +FGATC* FGATCMgr::GetATCPointer(const string& icao, const atc_type& type) { + if(airport_atc_map.find(icao) == airport_atc_map.end()) { + //cout << "Unable to find " << icao << ' ' << type << " in the airport_atc_map" << endl; + return NULL; + } + //cout << "In GetATCPointer, found " << icao << ' ' << type << endl; + AirportATC *a = airport_atc_map[icao]; + //cout << "a->lon = " << a->lon << '\n'; + //cout << "a->elev = " << a->elev << '\n'; + //cout << "a->tower_freq = " << a->tower_freq << '\n'; + switch(type) { + case TOWER: + if(a->tower_active) { + // Get the pointer from the list + return(FindInList(icao, type)); + } else { + ATCData data; + if(current_commlist->FindByFreq(a->lon, a->lat, a->elev, a->tower_freq, &data, TOWER)) { + FGTower* t = new FGTower; + t->SetData(&data); + atc_list.push_back(t); + a->tower_active = true; + airport_atc_map[icao] = a; + //cout << "Initing tower " << icao << " in GetATCPointer()\n"; + t->Init(); + return(t); + } else { + SG_LOG(SG_ATC, SG_ALERT, "ERROR - tower that should exist in FGATCMgr::GetATCPointer for airport " << icao << " not found"); + } + } + break; + case APPROACH: + break; + case ATIS: + SG_LOG(SG_ATC, SG_ALERT, "ERROR - ATIS station should not be requested from FGATCMgr::GetATCPointer"); + break; + case GROUND: + //cout << "IN CASE GROUND" << endl; + if(a->ground_active) { + // Get the pointer from the list + return(FindInList(icao, type)); + } else { + ATCData data; + if(current_commlist->FindByFreq(a->lon, a->lat, a->elev, a->ground_freq, &data, GROUND)) { + FGGround* g = new FGGround; + g->SetData(&data); + atc_list.push_back(g); + a->ground_active = true; + airport_atc_map[icao] = a; + g->Init(); + return(g); + } else { + SG_LOG(SG_ATC, SG_ALERT, "ERROR - ground control that should exist in FGATCMgr::GetATCPointer for airport " << icao << " not found"); + } + } + break; + case INVALID: + break; + case ENROUTE: + break; + case DEPARTURE: + break; + } + + SG_LOG(SG_ATC, SG_ALERT, "ERROR IN FGATCMgr - reached end of GetATCPointer"); + //cout << "ERROR IN FGATCMgr - reached end of GetATCPointer" << endl; + + return(NULL); +} + +// Return a pointer to an appropriate voice for a given type of ATC +// creating the voice if necessary - ie. make sure exactly one copy +// of every voice in use exists in memory. +// +// TODO - in the future this will get more complex and dole out country/airport +// specific voices, and possible make sure that the same voice doesn't get used +// at different airports in quick succession if a large enough selection are available. +FGATCVoice* FGATCMgr::GetVoicePointer(const atc_type& type) { + // TODO - implement me better - maintain a list of loaded voices and other voices!! + if(voice) { + switch(type) { + case ATIS: + if(voiceOK) { + return(v1); + } + case TOWER: + return(NULL); + case APPROACH: + return(NULL); + case GROUND: + return(NULL); + default: + return(NULL); + } + return(NULL); + } else { + return(NULL); + } +} + +// Search for ATC stations by frequency +void FGATCMgr::FreqSearch(int channel) { + int chan = channel - 1; // Convert to zero-based for the arrays + + ATCData data; + double freq = comm_node[chan]->getDoubleValue(); + lon = lon_node->getDoubleValue(); + lat = lat_node->getDoubleValue(); + elev = elev_node->getDoubleValue() * SG_FEET_TO_METER; + + // Query the data store and get the closest match if any + if(current_commlist->FindByFreq(lon, lat, elev, freq, &data)) { + // We have a match + // What's the logic? + // If this channel not previously valid then easy - add ATC to list + // If this channel was valid then - Have we tuned to a different service? + // If so - de-register one and add the other + if(comm_valid[chan]) { + if((comm_ident[chan] == data.ident) && (comm_type[chan] == data.type)) { + // Then we're still tuned into the same service so do nought and return + return; + } else { + // Something's changed - either the location or the service type + // We need to feed the channel in so we're not removing it if we're also tuned in on the other channel + CommRemoveFromList(comm_ident[chan], comm_type[chan], chan); + } + } + // At this point we can assume that we need to add the service. + comm_ident[chan] = data.ident; + comm_type[chan] = data.type; + comm_x[chan] = (double)data.x; + comm_y[chan] = (double)data.y; + comm_z[chan] = (double)data.z; + comm_lon[chan] = (double)data.lon; + comm_lat[chan] = (double)data.lat; + comm_elev[chan] = (double)data.elev; + comm_valid[chan] = true; + + // This was a switch-case statement but the compiler didn't like the new variable creation with it. + if(comm_type[chan] == ATIS) { + CommRegisterAirport(comm_ident[chan], chan, ATIS); + FGATC* app = FindInList(comm_ident[chan], ATIS); + if(app != NULL) { + // The station is already in the ATC list + //cout << "In list - flagging SetDisplay..." << endl; + comm_atc_ptr[chan] = app; + app->SetDisplay(); + } else { + // Generate the station and put in the ATC list + //cout << "Not in list - generating..." << endl; + FGATIS* a = new FGATIS; + a->SetData(&data); + comm_atc_ptr[chan] = a; + a->SetDisplay(); + //a->Init(); + atc_list.push_back(a); + } + } else if (comm_type[chan] == TOWER) { + //cout << "TOWER TOWER TOWER\n"; + CommRegisterAirport(comm_ident[chan], chan, TOWER); + //cout << "Done (TOWER)" << endl; + FGATC* app = FindInList(comm_ident[chan], TOWER); + if(app != NULL) { + // The station is already in the ATC list + SG_LOG(SG_GENERAL, SG_DEBUG, comm_ident[chan] << " is in list - flagging SetDisplay..."); + //cout << comm_ident[chan] << " is in list - flagging SetDisplay...\n"; + comm_atc_ptr[chan] = app; + app->SetDisplay(); + } else { + // Generate the station and put in the ATC list + SG_LOG(SG_GENERAL, SG_DEBUG, comm_ident[chan] << " is not in list - generating..."); + //cout << comm_ident[chan] << " is not in list - generating...\n"; + FGTower* t = new FGTower; + t->SetData(&data); + comm_atc_ptr[chan] = t; + //cout << "Initing tower in FreqSearch()\n"; + t->Init(); + t->SetDisplay(); + atc_list.push_back(t); + } + } else if (comm_type[chan] == GROUND) { + CommRegisterAirport(comm_ident[chan], chan, GROUND); + //cout << "Done (GROUND)" << endl; + FGATC* app = FindInList(comm_ident[chan], GROUND); + if(app != NULL) { + // The station is already in the ATC list + comm_atc_ptr[chan] = app; + app->SetDisplay(); + } else { + // Generate the station and put in the ATC list + FGGround* g = new FGGround; + g->SetData(&data); + comm_atc_ptr[chan] = g; + g->Init(); + g->SetDisplay(); + atc_list.push_back(g); + } + } else if (comm_type[chan] == APPROACH) { + // We have to be a bit more carefull here since approaches are also searched by area + CommRegisterAirport(comm_ident[chan], chan, APPROACH); + //cout << "Done (APPROACH)" << endl; + FGATC* app = FindInList(comm_ident[chan], APPROACH); + if(app != NULL) { + // The station is already in the ATC list + app->AddPlane("Player"); + app->SetDisplay(); + comm_atc_ptr[chan] = app; + } else { + // Generate the station and put in the ATC list + FGApproach* a = new FGApproach; + a->SetData(&data); + comm_atc_ptr[chan] = a; + a->Init(); + a->SetDisplay(); + a->AddPlane("Player"); + atc_list.push_back(a); + } + } + } else { + if(comm_valid[chan]) { + if(comm_type[chan] != APPROACH) { + // Currently approaches are removed by Alexander's out-of-range mechanism + CommRemoveFromList(comm_ident[chan], comm_type[chan], chan); + } + // Note that we *don't* call SetNoDisplay() here because the other comm channel + // might be tuned into the same station - this is handled by CommRemoveFromList(...) + comm_type[chan] = INVALID; + comm_atc_ptr[chan] = NULL; + comm_valid[chan] = false; + } + } +} + +// Search ATC stations by area in order that we appear 'on the radar' +void FGATCMgr::AreaSearch() { + // Search for Approach stations + comm_list_type approaches; + comm_list_iterator app_itr; + + lon = lon_node->getDoubleValue(); + lat = lat_node->getDoubleValue(); + elev = elev_node->getDoubleValue() * SG_FEET_TO_METER; + + // search stations in range + int num_app = current_commlist->FindByPos(lon, lat, elev, 100.0, &approaches, APPROACH); + if (num_app != 0) { + //cout << num_app << " approaches found in radiostack search !!!!" << endl; + + for(app_itr = approaches.begin(); app_itr != approaches.end(); app_itr++) { + + FGATC* app = FindInList(app_itr->ident, app_itr->type); + if(app != NULL) { + // The station is already in the ATC list + //cout << "In list adding player\n"; + app->AddPlane("Player"); + //app->Update(); + } else { + // Generate the station and put in the ATC list + FGApproach* a = new FGApproach; + a->SetData(&(*app_itr)); + //cout << "Adding player\n"; + a->AddPlane("Player"); + //a->Update(); + atc_list.push_back(a); + } + } + } + + // remove planes which are out of range + // TODO - I'm not entirely sure that this belongs here. + atc_list_iterator it = atc_list.begin(); + while(it != atc_list.end()) { + if((*it)->GetType() == APPROACH ) { + int np = (*it)->RemovePlane(); + // if approach has no planes left remove it from ATC list + if ( np == 0) { + //cout << "REMOVING AN APPROACH STATION WITH NO PLANES..." << endl; + (*it)->SetNoDisplay(); + (*it)->Update(0.00833); + delete (*it); + atc_list.erase(it); + atc_list_itr = atc_list.begin(); // Reset the persistent itr incase we've left it off the end. + break; // the other stations will be checked next time + } + } + ++it; + } +} diff --git a/src/ATCDCL/ATCmgr.hxx b/src/ATCDCL/ATCmgr.hxx new file mode 100644 index 000000000..9e8e1f837 --- /dev/null +++ b/src/ATCDCL/ATCmgr.hxx @@ -0,0 +1,212 @@ +// ATCMgr.hxx - definition of FGATCMgr +// - a global management class for FlightGear generated ATC +// +// Written by David Luff, started February 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_ATCMGR_HXX +#define _FG_ATCMGR_HXX + +#include <simgear/structure/subsystem_mgr.hxx> + +#include <Main/fg_props.hxx> +#include <GUI/gui.h> + +#include <string> +#include <list> +#include <map> + +#include "tower.hxx" +#include "approach.hxx" + +SG_USING_STD(string); +SG_USING_STD(list); +SG_USING_STD(map); + +// Structure for holding details of the ATC frequencies at a given airport, and whether they are in the active list or not. +// These can then be cross referenced with the commlists which are stored by frequency or bucket. +// Non-available services are denoted by a frequency of zero. +// These structures are only intended to be created for in-use airports, and removed when no longer needed. +struct AirportATC { + AirportATC(); + + float lon; + float lat; + float elev; + float atis_freq; + bool atis_active; + float tower_freq; + bool tower_active; + float ground_freq; + bool ground_active; + //float approach_freq; + //bool approach_active; + //float departure_freq; + //bool departure_active; + + // NOTE - the *_active flags determine whether the service is active in atc_list, + // *NOT* whether the tower etc is closed or not!!!! + + // Flags to ensure the stations don't get wrongly deactivated + bool set_by_AI; // true when the AI manager has activated this station + unsigned int numAI; // Ref count of the number of AI planes registered + bool set_by_comm[2][ATC_NUM_TYPES]; // true when the relevant comm_freq has activated this station and type +}; + +class FGATCMgr : public SGSubsystem +{ + +private: + + bool initDone; // Hack - guard against update getting called before init + + // A map of airport ID vs frequencies and ATC provision + typedef map < string, AirportATC* > airport_atc_map_type; + typedef airport_atc_map_type::iterator airport_atc_map_iterator; + typedef airport_atc_map_type::const_iterator airport_atc_map_const_iterator; + + airport_atc_map_type airport_atc_map; + airport_atc_map_iterator airport_atc_map_itr; + + // A list of pointers to all currently active ATC classes + typedef list <FGATC*> atc_list_type; + typedef atc_list_type::iterator atc_list_iterator; + typedef atc_list_type::const_iterator atc_list_const_iterator; + + // Everything put in this list should be created dynamically + // on the heap and ***DELETED WHEN REMOVED!!!!!*** + atc_list_type atc_list; + atc_list_iterator atc_list_itr; + // Any member function of FGATCMgr is permitted to leave this iterator pointing + // at any point in or at the end of the list. + // Hence any new access must explicitly first check for atc_list.end() before dereferencing. + + // Position of the Users Aircraft + double lon; + double lat; + double elev; + + // Type of ATC control that the user's radios are tuned to. + atc_type comm_type[2]; + + // Pointer to the ATC station that the user is currently tuned into. + FGATC* comm_atc_ptr[2]; + + double comm_freq[2]; + + // Pointers to users current communication frequencies. + SGPropertyNode_ptr comm_node[2]; + + // Pointers to current users position + SGPropertyNode_ptr lon_node; + SGPropertyNode_ptr lat_node; + SGPropertyNode_ptr elev_node; + + // Position of the ATC that the comm radios are tuned to in order to decide + // whether transmission will be received. + double comm_x[2], comm_y[2], comm_z[2], comm_lon[2], comm_lat[2], comm_elev[2]; + + double comm_range[2], comm_effective_range[2]; + bool comm_valid[2]; + string comm_ident[2]; + //string last_comm_ident[2]; + //string approach_ident; + bool last_in_range; + + //FGATIS atis; + //FGGround ground; + FGTower tower; + FGApproach approach; + //FGDeparture departure; + + // Voice related stuff + bool voice; // Flag - true if we are using voice +#ifdef ENABLE_AUDIO_SUPPORT + bool voiceOK; // Flag - true if at least one voice has loaded OK + FGATCVoice* v1; +#endif + +public: + + FGATCMgr(); + ~FGATCMgr(); + + void init(); + + void bind(); + + void unbind(); + + void update(double dt); + + // Returns true if the airport is found in the map + bool GetAirportATCDetails(const string& icao, AirportATC* a); + + // Return a pointer to a given sort of ATC at a given airport and activate if necessary + // Returns NULL if service doesn't exist - calling function should check for this. + FGATC* GetATCPointer(const string& icao, const atc_type& type); + + // Return a pointer to an appropriate voice for a given type of ATC + // creating the voice if necessary - ie. make sure exactly one copy + // of every voice in use exists in memory. + // + // TODO - in the future this will get more complex and dole out country/airport + // specific voices, and possible make sure that the same voice doesn't get used + // at different airports in quick succession if a large enough selection are available. + FGATCVoice* GetVoicePointer(const atc_type& type); + + atc_type GetComm1ATCType() { return(comm_type[0]); } + FGATC* GetComm1ATCPointer() { return(comm_atc_ptr[0]); } + atc_type GetComm2ATCType() { return(comm_type[1]); } + FGATC* GetComm2ATCPointer() { return(comm_atc_ptr[1]); } + + // Get the frequency of a given service at a given airport + // Returns zero if not found + unsigned short int GetFrequency(const string& ident, const atc_type& tp); + + // Register the fact that the AI system wants to activate an airport + bool AIRegisterAirport(const string& ident); + + // Register the fact that the comm radio is tuned to an airport + bool CommRegisterAirport(const string& ident, int chan, const atc_type& tp); + +private: + + // Remove a class from the atc_list and delete it from memory + // *if* no other comm channel or AI plane is using it. + void CommRemoveFromList(const string& id, const atc_type& tp, int chan); + + // Remove a class from the atc_list and delete it from memory + // Should be called from the above - not directly!! + void RemoveFromList(const string& id, const atc_type& tp); + + // Return a pointer to a class in the list given ICAO code and type + // (external interface to this is through GetATCPointer) + // Return NULL if the given service is not in the list + // - *** THE CALLING FUNCTION MUST CHECK FOR THIS *** + FGATC* FindInList(const string& id, const atc_type& tp); + + // Search the specified channel for stations on the same frequency and in range. + void FreqSearch(int channel); + + // Search ATC stations by area in order that we appear 'on the radar' + void AreaSearch(); + +}; + +#endif // _FG_ATCMGR_HXX diff --git a/src/ATCDCL/ATCutils.cxx b/src/ATCDCL/ATCutils.cxx new file mode 100644 index 000000000..c5a229fee --- /dev/null +++ b/src/ATCDCL/ATCutils.cxx @@ -0,0 +1,314 @@ +// ATCutils.cxx - Utility functions for the ATC / AI system +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <sstream> + +#include <math.h> +#include <simgear/math/point3d.hxx> +#include <simgear/constants.h> +#include <simgear/misc/sg_path.hxx> +#include <simgear/debug/logstream.hxx> +#include <plib/sg.h> +//#include <iomanip.h> + +#include <Airports/runways.hxx> +#include <Main/globals.hxx> + +#include "ATCutils.hxx" +#include "ATCProjection.hxx" + +static const string nums[10] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "niner"}; + +// Convert any number to spoken digits +string ConvertNumToSpokenDigits(const string &n) { + //cout << "n = " << n << endl; + static const string pt = "decimal"; + string str = ""; + + for(unsigned int i=0; i<n.length(); ++i) { + //cout << "n.substr(" << i << ",1 = " << n.substr(i,1) << endl; + if(n.substr(i,1) == " ") { + // do nothing + } else if(n.substr(i,1) == ".") { + str += pt; + } else { + str += nums[atoi((n.substr(i,1)).c_str())]; + } + if(i != (n.length()-1)) { // ie. don't add a space at the end. + str += " "; + } + } + return(str); +} + + +// Convert an integer to spoken digits +string ConvertNumToSpokenDigits(int n) { + std::ostringstream buf; + buf << n; + return(ConvertNumToSpokenDigits(buf.str())); +} + + +// Convert a 2 digit rwy number to a spoken-style string +string ConvertRwyNumToSpokenString(int n) { + // Basic error/sanity checking + while(n < 0) { + n += 36; + } + while(n > 36) { + n -= 36; + } + if(n == 0) { + n = 36; // Is this right? + } + + string str = ""; + int index = n/10; + str += nums[index]; + n -= (index * 10); + //str += "-"; + str += " "; //Changed this for the benefit of the voice token parser - prefer the "-" in the visual output though. + str += nums[n]; + return(str); +} + +// Assumes we get a two-digit string optionally appended with L, R or C +// eg 01 07L 29R 36 +// Anything else is not guaranteed to be handled correctly! +string ConvertRwyNumToSpokenString(const string &s) { + if(s.size() < 3) { + return(ConvertRwyNumToSpokenString(atoi(s.c_str()))); + } else { + string r = ConvertRwyNumToSpokenString(atoi(s.substr(0,2).c_str())); + if(s.substr(2,1) == "L") { + r += " left"; + } else if(s.substr(2,1) == "R") { + r += " right"; + } else if(s.substr(2,1) == "C") { + r += " center"; + } else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unknown suffix " << s.substr(2,1) << " from runway ID " << s << " in ConvertRwyNumToSpokenString(...)"); + } + return(r); + } +} + + +// Return the phonetic letter of a letter represented as an integer 1->26 +string GetPhoneticIdent(int i) { + // TODO - Check i is between 1 and 26 and wrap if necessary + return(GetPhoneticIdent(char('a' + (i-1)))); +} + +// Return the phonetic letter of a character in the range a-z or A-Z. +// Currently always returns prefixed by lowercase. +string GetPhoneticIdent(char c) { + c = tolower(c); + // TODO - Check c is between a and z and wrap if necessary + switch(c) { + case 'a' : return("alpha"); + case 'b' : return("bravo"); + case 'c' : return("charlie"); + case 'd' : return("delta"); + case 'e' : return("echo"); + case 'f' : return("foxtrot"); + case 'g' : return("golf"); + case 'h' : return("hotel"); + case 'i' : return("india"); + case 'j' : return("juliet"); + case 'k' : return("kilo"); + case 'l' : return("lima"); + case 'm' : return("mike"); + case 'n' : return("november"); + case 'o' : return("oscar"); + case 'p' : return("papa"); + case 'q' : return("quebec"); + case 'r' : return("romeo"); + case 's' : return("sierra"); + case 't' : return("tango"); + case 'u' : return("uniform"); + case 'v' : return("victor"); + case 'w' : return("whiskey"); + case 'x' : return("x-ray"); + case 'y' : return("yankee"); + case 'z' : return("zulu"); + } + // We shouldn't get here + return("Error"); +} + +// Get the compass direction associated with a heading in degrees +// Currently returns 8 direction resolution (N, NE, E etc...) +// Might be modified in future to return 4, 8 or 16 resolution but defaulting to 8. +string GetCompassDirection(double h) { + while(h < 0.0) h += 360.0; + while(h > 360.0) h -= 360.0; + if(h < 22.5 || h > 337.5) { + return("North"); + } else if(h < 67.5) { + return("North-East"); + } else if(h < 112.5) { + return("East"); + } else if(h < 157.5) { + return("South-East"); + } else if(h < 202.5) { + return("South"); + } else if(h < 247.5) { + return("South-West"); + } else if(h < 292.5) { + return("West"); + } else { + return("North-West"); + } +} + +//================================================================================================================ + +// Given two positions (lat & lon in degrees), get the HORIZONTAL separation (in meters) +double dclGetHorizontalSeparation(const Point3D& pos1, const Point3D& pos2) { + double x; //East-West separation + double y; //North-South separation + double z; //Horizontal separation - z = sqrt(x^2 + y^2) + + double lat1 = pos1.lat() * SG_DEGREES_TO_RADIANS; + double lon1 = pos1.lon() * SG_DEGREES_TO_RADIANS; + double lat2 = pos2.lat() * SG_DEGREES_TO_RADIANS; + double lon2 = pos2.lon() * SG_DEGREES_TO_RADIANS; + + y = sin(fabs(lat1 - lat2)) * SG_EQUATORIAL_RADIUS_M; + x = sin(fabs(lon1 - lon2)) * SG_EQUATORIAL_RADIUS_M * (cos((lat1 + lat2) / 2.0)); + z = sqrt(x*x + y*y); + + return(z); +} + +// Given a point and a line, get the HORIZONTAL shortest distance from the point to a point on the line. +// Expects to be fed orthogonal co-ordinates, NOT lat & lon ! +// The units of the separation will be those of the input. +double dclGetLinePointSeparation(double px, double py, double x1, double y1, double x2, double y2) { + double vecx = x2-x1; + double vecy = y2-y1; + double magline = sqrt(vecx*vecx + vecy*vecy); + double u = ((px-x1)*(x2-x1) + (py-y1)*(y2-y1)) / (magline * magline); + double x0 = x1 + u*(x2-x1); + double y0 = y1 + u*(y2-y1); + vecx = px - x0; + vecy = py - y0; + double d = sqrt(vecx*vecx + vecy*vecy); + if(d < 0) { + d *= -1; + } + return(d); +} + +// Given a position (lat/lon/elev), heading and vertical angle (degrees), and distance (meters), calculate the new position. +// This function assumes the world is spherical. If geodetic accuracy is required use the functions is sg_geodesy instead! +// Assumes that the ground is not hit!!! Expects heading and angle in degrees, distance in meters. +Point3D dclUpdatePosition(const Point3D& pos, double heading, double angle, double distance) { + //cout << setprecision(10) << pos.lon() << ' ' << pos.lat() << '\n'; + heading *= DCL_DEGREES_TO_RADIANS; + angle *= DCL_DEGREES_TO_RADIANS; + double lat = pos.lat() * DCL_DEGREES_TO_RADIANS; + double lon = pos.lon() * DCL_DEGREES_TO_RADIANS; + double elev = pos.elev(); + //cout << setprecision(10) << lon*DCL_RADIANS_TO_DEGREES << ' ' << lat*DCL_RADIANS_TO_DEGREES << '\n'; + + double horiz_dist = distance * cos(angle); + double vert_dist = distance * sin(angle); + + double north_dist = horiz_dist * cos(heading); + double east_dist = horiz_dist * sin(heading); + + //cout << distance << ' ' << horiz_dist << ' ' << vert_dist << ' ' << north_dist << ' ' << east_dist << '\n'; + + double delta_lat = asin(north_dist / (double)SG_EQUATORIAL_RADIUS_M); + double delta_lon = asin(east_dist / (double)SG_EQUATORIAL_RADIUS_M) * (1.0 / cos(lat)); // I suppose really we should use the average of the original and new lat but we'll assume that this will be good enough. + //cout << delta_lon*DCL_RADIANS_TO_DEGREES << ' ' << delta_lat*DCL_RADIANS_TO_DEGREES << '\n'; + lat += delta_lat; + lon += delta_lon; + elev += vert_dist; + //cout << setprecision(10) << lon*DCL_RADIANS_TO_DEGREES << ' ' << lat*DCL_RADIANS_TO_DEGREES << '\n'; + + //cout << setprecision(15) << DCL_DEGREES_TO_RADIANS * DCL_RADIANS_TO_DEGREES << '\n'; + + return(Point3D(lon*DCL_RADIANS_TO_DEGREES, lat*DCL_RADIANS_TO_DEGREES, elev)); +} + +// Get a heading in degrees from one lat/lon to another. +// This function assumes the world is spherical. If geodetic accuracy is required use the functions is sg_geodesy instead! +// Warning - at the moment we are not checking for identical points - currently it returns 0 in this instance. +double GetHeadingFromTo(const Point3D& A, const Point3D& B) { + double latA = A.lat() * DCL_DEGREES_TO_RADIANS; + double lonA = A.lon() * DCL_DEGREES_TO_RADIANS; + double latB = B.lat() * DCL_DEGREES_TO_RADIANS; + double lonB = B.lon() * DCL_DEGREES_TO_RADIANS; + double xdist = sin(lonB - lonA) * (double)SG_EQUATORIAL_RADIUS_M * cos((latA+latB)/2.0); + double ydist = sin(latB - latA) * (double)SG_EQUATORIAL_RADIUS_M; + double heading = atan2(xdist, ydist) * DCL_RADIANS_TO_DEGREES; + return heading < 0.0 ? heading + 360 : heading; +} + +// Given a heading (in degrees), bound it from 0 -> 360 +void dclBoundHeading(double &hdg) { + while(hdg < 0.0) { + hdg += 360.0; + } + while(hdg > 360.0) { + hdg -= 360.0; + } +} + +// smallest difference between two angles in degrees +// difference is negative if a1 > a2 and positive if a2 > a1 +double GetAngleDiff_deg( const double &a1, const double &a2) { + + double a3 = a2 - a1; + while (a3 < 180.0) a3 += 360.0; + while (a3 > 180.0) a3 -= 360.0; + + return a3; +} + +// Runway stuff +// Given a Point3D (lon/lat/elev) and an FGRunway struct, determine if the point lies on the runway +bool OnRunway(const Point3D& pt, const FGRunway& rwy) { + FGATCAlignedProjection ortho; + Point3D centre(rwy._lon, rwy._lat, 0.0); // We don't need the elev + ortho.Init(centre, rwy._heading); + + Point3D xyc = ortho.ConvertToLocal(centre); + Point3D xyp = ortho.ConvertToLocal(pt); + + //cout << "Length offset = " << fabs(xyp.y() - xyc.y()) << '\n'; + //cout << "Width offset = " << fabs(xyp.x() - xyc.x()) << '\n'; + + if((fabs(xyp.y() - xyc.y()) < ((rwy._length/2.0) + 5.0)) + && (fabs(xyp.x() - xyc.x()) < (rwy._width/2.0))) { + return(true); + } + + return(false); +} + diff --git a/src/ATCDCL/ATCutils.hxx b/src/ATCDCL/ATCutils.hxx new file mode 100644 index 000000000..80b155e08 --- /dev/null +++ b/src/ATCDCL/ATCutils.hxx @@ -0,0 +1,103 @@ +// ATCutils.hxx - Utility functions for the ATC / AI subsytem +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include <Airports/simple.hxx> +#include <Airports/runways.hxx> + +#include <math.h> +#include <simgear/math/point3d.hxx> +#include <string> +SG_USING_STD(string); + +// These are defined here because I had a problem with SG_DEGREES_TO_RADIANS +#define DCL_PI 3.1415926535f +#define DCL_DEGREES_TO_RADIANS (DCL_PI/180.0) +#define DCL_RADIANS_TO_DEGREES (180.0/DCL_PI) + +/******************************* +* +* Communication functions +* +********************************/ + +// Convert any number to spoken digits +string ConvertNumToSpokenDigits(const string &n); + +// Convert an integer to spoken digits +string ConvertNumToSpokenDigits(int n); + +// Convert a 2 digit rwy number to a spoken-style string +string ConvertRwyNumToSpokenString(int n); + +// Convert rwy number string to a spoken-style string +// eg "05L" to "zero five left" +// Assumes we get a two-digit string optionally appended with R, L, or C +// eg 01 07L 29R 36 +// Anything else is not guaranteed to be handled correctly! +string ConvertRwyNumToSpokenString(const string &s); + +// Return the phonetic letter of a letter represented as an integer 1->26 +string GetPhoneticIdent(int i); + +// Return the phonetic letter of a character in the range a-z or A-Z. +// Currently always returns prefixed by lowercase. +string GetPhoneticIdent(char c); + +// Get the compass direction associated with a heading in degrees +// Currently returns 8 direction resolution (N, NE, E etc...) +// Might be modified in future to return 4, 8 or 16 resolution but defaulting to 8. +string GetCompassDirection(double h); + +/******************************* +* +* Positional functions +* +********************************/ + +// Given two positions (lat & lon in degrees), get the HORIZONTAL separation (in meters) +double dclGetHorizontalSeparation(const Point3D& pos1, const Point3D& pos2); + +// Given a point and a line, get the HORIZONTAL shortest distance from the point to a point on the line. +// Expects to be fed orthogonal co-ordinates, NOT lat & lon ! +double dclGetLinePointSeparation(double px, double py, double x1, double y1, double x2, double y2); + +// Given a position (lat/lon/elev), heading, vertical angle, and distance, calculate the new position. +// Assumes that the ground is not hit!!! Expects heading and angle in degrees, distance in meters. +Point3D dclUpdatePosition(const Point3D& pos, double heading, double angle, double distance); + +// Get a heading from one lat/lon to another (in degrees) +double GetHeadingFromTo(const Point3D& A, const Point3D& B); + +// Given a heading (in degrees), bound it from 0 -> 360 +void dclBoundHeading(double &hdg); + +// smallest difference between two angles in degrees +// difference is negative if a1 > a2 and positive if a2 > a1 +double GetAngleDiff_deg( const double &a1, const double &a2); + +/**************** +* +* Runways +* +****************/ + +// Given a Point3D (lon/lat/elev) and an FGRunway struct, determine if the point lies on the runway +bool OnRunway(const Point3D& pt, const FGRunway& rwy); + diff --git a/src/ATCDCL/Makefile.am b/src/ATCDCL/Makefile.am new file mode 100644 index 000000000..c560d1643 --- /dev/null +++ b/src/ATCDCL/Makefile.am @@ -0,0 +1,23 @@ +noinst_LIBRARIES = libATC.a + +libATC_a_SOURCES = \ + ATC.hxx ATC.cxx \ + atis.hxx atis.cxx \ + tower.hxx tower.cxx \ + approach.hxx approach.cxx \ + ground.hxx ground.cxx \ + commlist.hxx commlist.cxx \ + ATCDialog.hxx ATCDialog.cxx \ + ATCVoice.hxx ATCVoice.cxx \ + ATCmgr.hxx ATCmgr.cxx \ + ATCutils.hxx ATCutils.cxx \ + ATCProjection.hxx ATCProjection.cxx \ + AIMgr.hxx AIMgr.cxx \ + AIEntity.hxx AIEntity.cxx \ + AIPlane.hxx AIPlane.cxx \ + AILocalTraffic.hxx AILocalTraffic.cxx \ + AIGAVFRTraffic.hxx AIGAVFRTraffic.cxx \ + transmission.hxx transmission.cxx \ + transmissionlist.hxx transmissionlist.cxx + +INCLUDES = -I$(top_srcdir) -I$(top_srcdir)/src diff --git a/src/ATCDCL/approach.cxx b/src/ATCDCL/approach.cxx new file mode 100644 index 000000000..44784abf1 --- /dev/null +++ b/src/ATCDCL/approach.cxx @@ -0,0 +1,759 @@ +// FGApproach - a class to provide approach control at larger airports. +// +// Written by Alexander Kappes, started March 2002. +// +// Copyright (C) 2002 Alexander Kappes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "approach.hxx" +#include "transmission.hxx" +#include "transmissionlist.hxx" +#include "ATCDialog.hxx" + +#include <Airports/runways.hxx> +#include <simgear/constants.h> +#include <simgear/math/polar3d.hxx> +#include <simgear/misc/sg_path.hxx> + +#include <Environment/environment_mgr.hxx> +#include <Environment/environment.hxx> + + +#include <GUI/gui.h> + +//Constructor +FGApproach::FGApproach(){ + comm1_node = fgGetNode("/instrumentation/comm[0]/frequencies/selected-mhz", true); + comm2_node = fgGetNode("/instrumentation/comm[1]/frequencies/selected-mhz", true); + + _type = APPROACH; + + num_planes = 0; + lon_node = fgGetNode("/position/longitude-deg", true); + lat_node = fgGetNode("/position/latitude-deg", true); + elev_node = fgGetNode("/position/altitude-ft", true); + hdg_node = fgGetNode("/orientation/heading-deg", true); + speed_node = fgGetNode("/velocities/airspeed-kt", true); + etime_node = fgGetNode("/sim/time/elapsed-sec", true); + + first = true; + active_runway = ""; + int i; + for ( i=0; i<max_planes; i++) { + planes[i].contact = 0; + planes[i].wpn = 0; + planes[i].dnwp = -999.; + planes[i].on_crs = true; + planes[i].turn_rate = 10.0; + planes[i].desc_rate = 1000.0; + planes[i].clmb_rate = 500.0; + planes[i].tlm = 0.0; + planes[i].lmc.c1 = 0; + planes[i].lmc.c2 = 0; + planes[i].lmc.c3 = -1; + planes[i].wp_change = false; + } +} + +//Destructor +FGApproach::~FGApproach(){ +} + +void FGApproach::Init() { +} + + + +// ============================================================================ +// the main update function +// ============================================================================ +void FGApproach::Update(double dt) { + + const int max_trans = 20; + FGTransmission tmissions[max_trans]; + int wpn; + atc_type station = APPROACH; + TransCode code; + TransPar TPar; + int i,j; + //double course, d, + double adif, datp; + //char buf[10]; + string message; + //static string atcmsg1[10]; + //static string atcmsg2[10]; + string mentry; + string transm; + TransPar tpars; + //static bool TransDisplayed = false; + + update_plane_dat(); + if ( active_runway == "" ) get_active_runway(); + + double comm1_freq = comm1_node->getDoubleValue(); + + //bool DisplayTransmissions = true; + + for (i=0; i<num_planes; i++) { + if ( planes[i].ident == "Player") { + station = APPROACH; + tpars.station = name; + tpars.callsign = "Player"; + tpars.airport = ident; + + //cout << "ident = " << ident << " name = " << name << '\n'; + + int num_trans = 0; + // is the frequency of the station tuned in? + if ( freq == (int)(comm1_freq*100.0 + 0.5) ) { + current_transmissionlist->query_station( station, tmissions, max_trans, num_trans ); + // loop over all transmissions for station + for ( j=0; j<=num_trans-1; j++ ) { + code = tmissions[j].get_code(); + //cout << "code is " << code.c1 << " " << code.c2 << " " << code.c3 << '\n'; + // select proper transmissions + if(code.c3 != 2) { // DCL - hack to prevent request crossing airspace being displayed since this isn't implemented yet. + if ( ( code.c2 == -1 && planes[i].lmc.c3 == 0 ) || + ( code.c1 == 0 && code.c2 == planes[i].lmc.c2 ) ) { + mentry = current_transmissionlist->gen_text(station, code, tpars, false); + transm = current_transmissionlist->gen_text(station, code, tpars, true); + // is the transmission already registered? + if (!current_atcdialog->trans_reg( ident, transm, APPROACH )) { + current_atcdialog->add_entry( ident, transm, mentry, APPROACH, 0 ); + } + } + } + } + } + } + } + + for ( i=0; i<num_planes; i++ ) { + //cout << "TPar.airport = " << TPar.airport << " TPar.station = " << TPar.station << " TPar.callsign = " << TPar.callsign << '\n'; + //if ( planes[i].ident == TPar.callsign && name == TPar.airport && TPar.station == "approach" ) { + //if ( TPar.request && TPar.intention == "landing" && ident == TPar.intid) { + if(planes[i].ident == "Player" && fgGetBool("/sim/atc/opt0")) { + //cout << "Landing requested\n"; + fgSetBool("/sim/atc/opt0", false); + planes[i].wpn = 0; + // =========================== + // === calculate waypoints === + // =========================== + calc_wp( i ); + update_param( i ); + wpn = planes[i].wpn-1; + planes[i].aalt = planes[i].wpts[wpn-1][2]; + planes[i].ahdg = planes[i].wpts[wpn][4]; + + // generate the message + code.c1 = 1; + code.c2 = 1; + code.c3 = 0; + adif = angle_diff_deg( planes[i].hdg, planes[i].ahdg ); + tpars.station = name; + tpars.callsign = "Player"; + if ( adif < 0 ) tpars.tdir = 1; + else tpars.tdir = 2; + tpars.heading = planes[i].ahdg; + if (planes[i].alt-planes[i].aalt > 100.0) tpars.VDir = 1; + else if (planes[i].alt-planes[i].aalt < -100.0) tpars.VDir = 3; + else tpars.VDir = 2; + tpars.alt = planes[i].aalt; + message = current_transmissionlist->gen_text(station, code, tpars, true ); + //cout << message << '\n'; + set_message(message); + planes[i].lmc = code; + planes[i].tlm = etime_node->getDoubleValue(); + planes[i].on_crs = true; + planes[i].contact = 1; + } + //} + + //if(1) { + if ( planes[i].contact == 1 ) { + // ========================= + // === update parameters === + // ========================= + update_param( i ); + //cout << planes[i].brg << " " << planes[i].dist << " " << planes[i].wpts[wpn+1][0] + //<< " " << planes[i].wpts[wpn+1][1] << " " << planes[i].wpts[wpn+1][4] + //cout << wpn << " distance to current course = " << planes[i].dcc << endl; + //cout << etime_node->getDoubleValue() << endl; + + // ========================= + // === reached waypoint? === + // ========================= + wpn = planes[i].wpn-2; + adif = angle_diff_deg( planes[i].hdg, planes[i].wpts[wpn][4] ) + * SGD_DEGREES_TO_RADIANS; + datp = 2*sin(fabs(adif)/2.0)*sin(fabs(adif)/2.0) * + planes[i].spd/3600. * planes[i].turn_rate + + planes[i].spd/3600. * 3.0; + //cout << adif/SGD_DEGREES_TO_RADIANS << " " + // << datp << " " << planes[i].dnc << " " << planes[i].dcc <<endl; + if ( fabs(planes[i].dnc) < datp ) { + //if ( fabs(planes[i].dnc) < 0.3 && planes[i].dnwp < 1.0 ) { + //cout << "Reached next waypoint!\n"; + planes[i].wpn -= 1; + wpn = planes[i].wpn-1; + planes[i].ahdg = planes[i].wpts[wpn][4]; + planes[i].aalt = planes[i].wpts[wpn-1][2]; + planes[i].wp_change = true; + + // generate the message + adif = angle_diff_deg( planes[i].hdg, planes[i].ahdg ); + tpars.station = name; + tpars.callsign = "Player"; + if ( adif < 0 ) tpars.tdir = 1; + else tpars.tdir = 2; + tpars.heading = planes[i].ahdg; + + if ( wpn-1 != 0) { + code.c1 = 1; + code.c2 = 1; + code.c3 = 0; + if (planes[i].alt-planes[i].aalt > 100.0) tpars.VDir = 1; + else if (planes[i].alt-planes[i].aalt < -100.0) tpars.VDir = 3; + else tpars.VDir = 2; + tpars.alt = planes[i].aalt; + message = current_transmissionlist->gen_text(station, code, tpars, true ); + //cout << "Approach transmitting...\n"; + //cout << message << endl; + set_message(message); + } + else { + code.c1 = 1; + code.c2 = 3; + code.c3 = 0; + tpars.runway = active_runway; + message = current_transmissionlist->gen_text(station, code, tpars, true); + //cout << "Approach transmitting 2 ...\n"; + //cout << message << endl; + set_message(message); + } + planes[i].lmc = code; + planes[i].tlm = etime_node->getDoubleValue(); + planes[i].on_crs = true; + + update_param( i ); + } + + // ========================= + // === come off course ? === + // ========================= + if ( fabs(planes[i].dcc) > 1.0 && + ( !planes[i].wp_change || etime_node->getDoubleValue() - planes[i].tlm > tbm ) ) { + //cout << "Off course!\n"; + if ( planes[i].on_crs ) { + if ( planes[i].dcc < 0) { + planes[i].ahdg += 30.0; + } + else { + planes[i].ahdg -= 30.0; + } + if (planes[i].ahdg > 360.0) planes[i].ahdg -= 360.0; + else if (planes[i].ahdg < 0.0) planes[i].ahdg += 360.0; + } + //cout << planes[i].on_crs << " " + // << angle_diff_deg( planes[i].hdg, planes[i].ahdg) << " " + // << etime_node->getDoubleValue() << " " + // << planes[i].tlm << endl; + // generate the message + if ( planes[i].on_crs || + ( fabs(angle_diff_deg( planes[i].hdg, planes[i].ahdg )) > 30.0 && + etime_node->getDoubleValue() - planes[i].tlm > tbm) ) { + // generate the message + code.c1 = 1; + code.c2 = 4; + code.c3 = 0; + adif = angle_diff_deg( planes[i].hdg, planes[i].ahdg ); + tpars.station = name; + tpars.callsign = "Player"; + tpars.miles = fabs(planes[i].dcc); + if ( adif < 0 ) tpars.tdir = 1; + else tpars.tdir = 2; + tpars.heading = planes[i].ahdg; + message = current_transmissionlist->gen_text(station, code, tpars, true); + //cout << "Approach transmitting 3 ...\n"; + //cout << message << '\n'; + set_message(message); + planes[i].lmc = code; + planes[i].tlm = etime_node->getDoubleValue(); + } + + planes[i].on_crs = false; + } + else if ( !planes[i].on_crs ) { + //cout << "Off course 2!\n"; + wpn = planes[i].wpn-1; + adif = angle_diff_deg( planes[i].hdg, planes[i].wpts[wpn][4] ) + * SGD_DEGREES_TO_RADIANS; + datp = 2*sin(fabs(adif)/2.0)*sin(fabs(adif)/2.0) * + planes[i].spd/3600. * planes[i].turn_rate + + planes[i].spd/3600. * 3.0; + if ( fabs(planes[i].dcc) < datp ) { + planes[i].ahdg = fabs(planes[i].wpts[wpn][4]); + + // generate the message + code.c1 = 1; + code.c2 = 2; + code.c3 = 0; + tpars.station = name; + tpars.callsign = "Player"; + if ( adif < 0 ) tpars.tdir = 1; + else tpars.tdir = 2; + tpars.heading = planes[i].ahdg; + message = current_transmissionlist->gen_text(station, code, tpars, true); + //cout << "Approach transmitting 4 ...\n"; + //cout << message << '\n'; + set_message(message); + planes[i].lmc = code; + planes[i].tlm = etime_node->getDoubleValue(); + + planes[i].on_crs = true; + } + } + else if ( planes[i].wp_change ) { + planes[i].wp_change = false; + } + + // =================================================================== + // === Less than two minutes away from touchdown? -> Contact Tower === + // =================================================================== + if ( planes[i].wpn == 2 && planes[i].dnwp < planes[i].spd/60.*2.0 ) { + + double freq = 121.95; // Hardwired - FIXME + // generate message + code.c1 = 1; + code.c2 = 5; + code.c3 = 0; + tpars.station = name; + tpars.callsign = "Player"; + tpars.freq = freq; + message = current_transmissionlist->gen_text(station, code, tpars, true); + //cout << "Approach transmitting 5 ...\n"; + //cout << message << '\n'; + set_message(message); + planes[i].lmc = code; + planes[i].tlm = etime_node->getDoubleValue(); + + planes[i].contact = 2; + } + } + } +} + + +// ============================================================================ +// update course parameters +// ============================================================================ +void FGApproach::update_param( const int &i ) { + + double course, d; + + int wpn = planes[i].wpn-1; // this is the current waypoint + + planes[i].dcc = calc_psl_dist(planes[i].brg, planes[i].dist, + planes[i].wpts[wpn][0], planes[i].wpts[wpn][1], + planes[i].wpts[wpn][4]); + planes[i].dnc = calc_psl_dist(planes[i].brg, planes[i].dist, + planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], + planes[i].wpts[wpn-1][4]); + calc_hd_course_dist(planes[i].brg, planes[i].dist, + planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], + &course, &d); + planes[i].dnwp = d; + +} + +// ============================================================================ +// smallest difference between two angles in degree +// difference is negative if a1 > a2 and positive if a2 > a1 +// =========================================================================== +double FGApproach::angle_diff_deg( const double &a1, const double &a2) { + + double a3 = a2 - a1; + if (a3 < 180.0) a3 += 360.0; + if (a3 > 180.0) a3 -= 360.0; + + return a3; +} + +// ============================================================================ +// calculate waypoints +// ============================================================================ +void FGApproach::calc_wp( const int &i ) { + + int j; + double course, d, cd, a1; + + int wpn = planes[i].wpn; + // waypoint 0: Threshold of active runway + calc_gc_course_dist(Point3D(lon*SGD_DEGREES_TO_RADIANS, lat*SGD_DEGREES_TO_RADIANS, 0.0), + Point3D(active_rw_lon*SGD_DEGREES_TO_RADIANS,active_rw_lat*SGD_DEGREES_TO_RADIANS, 0.0 ), + &course, &d); + double d1 = active_rw_hdg+180.0; + if ( d1 > 360.0 ) d1 -=360.0; + calc_cd_head_dist(360.0-course*SGD_RADIANS_TO_DEGREES, d/SG_NM_TO_METER, + d1, active_rw_len/SG_NM_TO_METER/2.0, + &planes[i].wpts[wpn][0], &planes[i].wpts[wpn][1]); + planes[i].wpts[wpn][2] = elev; + planes[i].wpts[wpn][4] = 0.0; + planes[i].wpts[wpn][5] = 0.0; + wpn += 1; + + // ====================== + // horizontal navigation + // ====================== + // waypoint 1: point for turning onto final + calc_cd_head_dist(planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], d1, lfl, + &planes[i].wpts[wpn][0], &planes[i].wpts[wpn][1]); + calc_hd_course_dist(planes[i].wpts[wpn][0], planes[i].wpts[wpn][1], + planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], + &course, &d); + planes[i].wpts[wpn][4] = course; + planes[i].wpts[wpn][5] = d; + wpn += 1; + + // calculate course and distance from plane position to waypoint 1 + calc_hd_course_dist(planes[i].brg, planes[i].dist, planes[i].wpts[1][0], + planes[i].wpts[1][1], &course, &d); + // check if airport is not between plane and waypoint 1 and + // DCA to airport on course to waypoint 1 is larger than 10 miles + double zero = 0.0; + if ( fabs(angle_diff_deg( planes[i].wpts[1][0], planes[i].brg )) < 90.0 || + calc_psl_dist( zero, zero, planes[i].brg, planes[i].dist, course ) > 10.0 ) { + // check if turning angle at waypoint 1 would be > max_ta + if ( fabs(angle_diff_deg( planes[i].wpts[1][4], course )) > max_ta ) { + cd = calc_psl_dist(planes[i].brg, planes[i].dist, + planes[i].wpts[1][0], planes[i].wpts[1][1], + planes[i].wpts[1][4]); + a1 = atan2(cd,planes[i].wpts[1][1]); + planes[i].wpts[wpn][0] = planes[i].wpts[1][0] - a1/SGD_DEGREES_TO_RADIANS; + if ( planes[i].wpts[wpn][0] < 0.0) planes[i].wpts[wpn][0] += 360.0; + if ( planes[i].wpts[wpn][0] > 360.0) planes[i].wpts[wpn][0] -= 360.0; + planes[i].wpts[wpn][1] = fabs(cd) / sin(fabs(a1)); + calc_hd_course_dist(planes[i].wpts[wpn][0], planes[i].wpts[wpn][1], + planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], + &course, &d); + planes[i].wpts[wpn][4] = course; + planes[i].wpts[wpn][5] = d; + wpn += 1; + + calc_hd_course_dist(planes[i].brg, planes[i].dist, planes[i].wpts[wpn-1][0], + planes[i].wpts[wpn-1][1], &course, &d); + } + } else { + double leg = 10.0; + a1 = atan2(planes[i].wpts[1][1], leg ); + + if ( angle_diff_deg(planes[i].brg,planes[i].wpts[1][0]) < 0 ) + planes[i].wpts[wpn][0] = planes[i].wpts[1][0] + a1/SGD_DEGREES_TO_RADIANS; + else planes[i].wpts[wpn][0] = planes[i].wpts[1][0] - a1/SGD_DEGREES_TO_RADIANS; + + planes[i].wpts[wpn][1] = sqrt( planes[i].wpts[1][1]*planes[i].wpts[1][1] + leg*leg ); + calc_hd_course_dist(planes[i].wpts[wpn][0], planes[i].wpts[wpn][1], + planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], + &course, &d); + planes[i].wpts[wpn][4] = course; + planes[i].wpts[wpn][5] = d; + wpn += 1; + + calc_hd_course_dist(planes[i].brg, planes[i].dist, + planes[i].wpts[wpn-1][0], planes[i].wpts[wpn-1][1], + &course, &d); + } + + planes[i].wpts[wpn][0] = planes[i].brg; + planes[i].wpts[wpn][1] = planes[i].dist; + planes[i].wpts[wpn][2] = planes[i].alt; + planes[i].wpts[wpn][4] = course; + planes[i].wpts[wpn][5] = d; + wpn += 1; + + planes[i].wpn = wpn; + + // Now check if legs are too short or if legs can be shortend + // legs must be at least 2 flight minutes long + double mdist = planes[i].spd / 60.0 * 2.0; + for ( j=2; j<wpn-1; ++j ) { + if ( planes[i].wpts[j][1] < mdist) { + } + } + + // ==================== + // vertical navigation + // ==================== + double alt = elev+3000.0; + planes[i].wpts[1][2] = round_alt( true, alt ); + for ( j=2; j<wpn-1; ++j ) { + double dalt = planes[i].alt - planes[i].wpts[j-1][2]; + if ( dalt > 0 ) { + alt = planes[i].wpts[j-1][2] + + (planes[i].wpts[j][5] / planes[i].spd) * 60.0 * planes[i].desc_rate; + planes[i].wpts[j][2] = round_alt( false, alt ); + if ( planes[i].wpts[j][2] > planes[i].alt ) + planes[i].wpts[j][2] = round_alt( false, planes[i].alt ); + } + else { + planes[i].wpts[j][2] = planes[i].wpts[1][2]; + } + } + + cout << "Plane position: " << planes[i].brg << " " << planes[i].dist << endl; + for ( j=0; j<wpn; ++j ) { + cout << "Waypoint " << j << endl; + cout << "------------------" << endl; + cout << planes[i].wpts[j][0] << " " << planes[i].wpts[j][1] + << " " << planes[i].wpts[j][2] << " " << planes[i].wpts[j][5]; + cout << endl << endl; + } + +} + + +// ============================================================================ +// round altitude value to next highest/lowest 500 feet +// ============================================================================ +double FGApproach::round_alt( const bool hl, double alt ) { + + alt = alt/1000.0; + if ( hl ) { + if ( alt > (int)(alt)+0.5 ) alt = ((int)(alt)+1)*1000.0; + else alt = ((int)(alt)+0.5)*1000.0; + } + else { + if ( alt > (int)(alt)+0.5 ) alt = ((int)(alt)+0.5)*1000.0; + else alt = ((int)(alt))*1000.0; + } + + return alt; +} + + +// ============================================================================ +// get active runway +// ============================================================================ +void FGApproach::get_active_runway() { + //cout << "Entering FGApproach::get_active_runway()\n"; + + FGEnvironment stationweather = + ((FGEnvironmentMgr *)globals->get_subsystem("environment")) + ->getEnvironment(lat, lon, elev); + + double hdg = stationweather.get_wind_from_heading_deg(); + + FGRunway runway; + if ( globals->get_runways()->search( ident, int(hdg), &runway) ) { + active_runway = runway._rwy_no; + active_rw_hdg = runway._heading; + active_rw_lon = runway._lon; + active_rw_lat = runway._lat; + active_rw_len = runway._length; + //cout << "Active runway is: " << active_runway << " heading = " + // << active_rw_hdg + // << " lon = " << active_rw_lon + // << " lat = " << active_rw_lat <<endl; + } + else cout << "FGRunways search failed\n"; + +} + +// ======================================================================== +// update infos about plane +// ======================================================================== +void FGApproach::update_plane_dat() { + + //cout << "Update Approach " << ident << " " << num_planes << " registered" << endl; + // update plane positions + int i; + for (i=0; i<num_planes; i++) { + planes[i].lon = lon_node->getDoubleValue(); + planes[i].lat = lat_node->getDoubleValue(); + planes[i].alt = elev_node->getDoubleValue(); + planes[i].hdg = hdg_node->getDoubleValue(); + planes[i].spd = speed_node->getDoubleValue(); + + /*Point3D aircraft = sgGeodToCart( Point3D(planes[i].lon*SGD_DEGREES_TO_RADIANS, + planes[i].lat*SGD_DEGREES_TO_RADIANS, + planes[i].alt*SG_FEET_TO_METER) );*/ + double course, distance; + calc_gc_course_dist(Point3D(lon*SGD_DEGREES_TO_RADIANS, lat*SGD_DEGREES_TO_RADIANS, 0.0), + Point3D(planes[i].lon*SGD_DEGREES_TO_RADIANS,planes[i].lat*SGD_DEGREES_TO_RADIANS, 0.0 ), + &course, &distance); + planes[i].dist = distance/SG_NM_TO_METER; + planes[i].brg = 360.0-course*SGD_RADIANS_TO_DEGREES; + + //cout << "Plane Id: " << planes[i].ident << " Distance to " << ident + // << " is " << planes[i].dist << " miles " << "Bearing " << planes[i].brg << endl; + + } +} + +// ======================================================================= +// Add plane to Approach list +// ======================================================================= +void FGApproach::AddPlane(const string& pid) { + + int i; + for ( i=0; i<num_planes; i++) { + if ( planes[i].ident == pid) { + //cout << "Plane already registered: " << planes[i].ident << ' ' << ident << ' ' << num_planes << endl; + return; + } + } + planes[num_planes].ident = pid; + ++num_planes; + //cout << "Plane added to list: " << ident << " " << num_planes << endl; + return; +} + +// ================================================================================ +// closest distance between a point (h1,d1) and a straigt line (h2,d2,h3) in 2 dim. +// ================================================================================ +double FGApproach::calc_psl_dist(const double &h1, const double &d1, + const double &h2, const double &d2, + const double &h3) +{ + double a1 = h1 * SGD_DEGREES_TO_RADIANS; + double a2 = h2 * SGD_DEGREES_TO_RADIANS; + double a3 = h3 * SGD_DEGREES_TO_RADIANS; + double x1 = cos(a1) * d1; + double y1 = sin(a1) * d1; + double x2 = cos(a2) * d2; + double y2 = sin(a2) * d2; + double x3 = cos(a3); + double y3 = sin(a3); + + // formula: dis = sqrt( (v1-v2)**2 - ((v1-v2)*v3)**2 ); vi = (xi,yi) + double val1 = (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2); + double val2 = ((x1-x2)*x3 + (y1-y2)*y3) * ((x1-x2)*x3 + (y1-y2)*y3); + double dis = val1 - val2; + // now get sign for offset + //cout << x1 << " " << x2 << " " << y1 << " " << y2 << " " + // << x3 << " " << y3 << " " + // << val1 << " " << val2 << " " << dis << endl; + x3 *= sqrt(val2); + y3 *= sqrt(val2); + double da = fabs(atan2(y3,x3) - atan2(y1-y2,x1-x2)); + if ( da > SGD_PI ) da -= SGD_2PI; + if ( fabs(da) > SGD_PI_2) { + //if ( x3*(x1-x2) < 0.0 && y3*(y1-y2) < 0.0) { + x3 *= -1.0; + y3 *= -1.0; + } + //cout << x3 << " " << y3 << endl; + double dis1 = x1-x2-x3; + double dis2 = y1-y2-y3; + dis = sqrt(dis); + da = atan2(dis2,dis1); + if ( da < 0.0 ) da += SGD_2PI; + if ( da < a3 ) dis *= -1.0; + //cout << dis1 << " " << dis2 << " " << da*SGD_RADIANS_TO_DEGREES << " " << h3 + // << " " << sqrt(dis1*dis1 + dis2*dis2) << " " << dis << endl; + //cout << atan2(dis2,dis1)*SGD_RADIANS_TO_DEGREES << " " << dis << endl; + + return dis; +} + + +// ======================================================================== +// Calculate new bear/dist given starting bear/dis, and offset radial, +// and distance. +// ======================================================================== +void FGApproach::calc_cd_head_dist(const double &h1, const double &d1, + const double &course, const double &dist, + double *h2, double *d2) +{ + double a1 = h1 * SGD_DEGREES_TO_RADIANS; + double a2 = course * SGD_DEGREES_TO_RADIANS; + double x1 = cos(a1) * d1; + double y1 = sin(a1) * d1; + double x2 = cos(a2) * dist; + double y2 = sin(a2) * dist; + + *d2 = sqrt((x1+x2)*(x1+x2) + (y1+y2)*(y1+y2)); + *h2 = atan2( (y1+y2), (x1+x2) ) * SGD_RADIANS_TO_DEGREES; + if ( *h2 < 0 ) *h2 = *h2+360; +} + + + +// ======================================================================== +// get heading and distance between two points; point1 ---> point2 +// ======================================================================== +void FGApproach::calc_hd_course_dist(const double &h1, const double &d1, + const double &h2, const double &d2, + double *course, double *dist) +{ + double a1 = h1 * SGD_DEGREES_TO_RADIANS; + double a2 = h2 * SGD_DEGREES_TO_RADIANS; + double x1 = cos(a1) * d1; + double y1 = sin(a1) * d1; + double x2 = cos(a2) * d2; + double y2 = sin(a2) * d2; + + *dist = sqrt( (y2-y1)*(y2-y1) + (x2-x1)*(x2-x1) ); + *course = atan2( (y2-y1), (x2-x1) ) * SGD_RADIANS_TO_DEGREES; + if ( *course < 0 ) *course = *course+360; + //cout << x1 << " " << y1 << " " << x2 << " " << y2 << " " << *dist << " " << *course << endl; +} + + + +int FGApproach::RemovePlane() { + + // first check if anything has to be done + bool rmplane = false; + int i; + + for (i=0; i<num_planes; i++) { + if (planes[i].dist > range*SG_NM_TO_METER) { + rmplane = true; + break; + } + } + if (!rmplane) return num_planes; + + // now make a copy of the plane list + PlaneApp tmp[max_planes]; + for (i=0; i<num_planes; i++) { + tmp[i] = planes[i]; + } + + int np = 0; + // now check which planes are still in range + for (i=0; i<num_planes; i++) { + if (tmp[i].dist <= range*SG_NM_TO_METER) { + planes[np] = tmp[i]; + np += 1; + } + } + num_planes = np; + + return num_planes; +} + + +void FGApproach::set_message(const string &msg) +{ + fgSetString("/sim/messages/approach", msg.c_str()); +} + diff --git a/src/ATCDCL/approach.hxx b/src/ATCDCL/approach.hxx new file mode 100644 index 000000000..9aec71d01 --- /dev/null +++ b/src/ATCDCL/approach.hxx @@ -0,0 +1,231 @@ +// approach.hxx -- Approach class +// +// Written by Alexander Kappes, started March 2002. +// +// Copyright (C) 2002 Alexander Kappes +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#ifndef _FG_APPROACH_HXX +#define _FG_APPROACH_HXX + +#include <stdio.h> + +#include <simgear/compiler.h> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/magvar/magvar.hxx> +#include <simgear/timing/sg_time.hxx> +#include <simgear/bucket/newbucket.hxx> + +#include <Main/fg_props.hxx> + +#ifdef SG_HAVE_STD_INCLUDES +# include <istream> +#include <iomanip> +#elif defined( SG_HAVE_NATIVE_SGI_COMPILERS ) +# include <iostream.h> +#elif defined( __BORLANDC__ ) +# include <iostream> +#else +# include <istream.h> +#include <iomanip.h> +#endif + +#if ! defined( SG_HAVE_NATIVE_SGI_COMPILERS ) +SG_USING_STD(istream); +#endif + +SG_USING_STD(string); + +#include "ATC.hxx" +#include "transmission.hxx" + +//DCL - a complete guess for now. +#define FG_APPROACH_DEFAULT_RANGE 100 + +// Contains all the information about a plane that the approach control needs +const int max_planes = 20; // max number of planes on the stack +const int max_wp = 10; // max number of waypoints for approach phase +const double max_ta = 130; // max turning angle for plane during approach +const double tbm = 2.0; // min time (in sec) between two messages +const double lfl = 10.0; // length of final leg + +struct PlaneApp { + + // variables for plane if it's on the radar + string ident; // indentification of plane + double lon; // longitude in degrees + double lat; // latitude in degrees + double alt; // Altitute above sea level in feet + double hdg; // heading of plane in degrees + double dist; // distance to airport in miles + double brg; // bearing relative to airport in degrees + double spd; // speed above ground + int contact; // contact with approach established? + // 0 = no contact yet + // 1 = in contact + // 2 = handed off to tower + double turn_rate; // standard turning rate of the plane in seconds per degree + double desc_rate; // standard descent rate of the plane in feets per minute + double clmb_rate; // standard climb rate of the plane in feets per minute + + // additional variables if contact has been established + int wpn; // number of waypoints + double wpts[max_wp][6]; // assigned waypoints for approach phase + // first wp in list is airport + // last waypoint point at which contact was established + // second index: 0 = bearing to airport + // second index: 1 = distance to airport + // second index: 2 = alt + // second index: 3 = ETA + // second index: 4 = heading to next waypoint + // second index: 5 = distance to next waypoint + + double dnwp; // distance to next waypoint + double dcc; // closest distance to current assigned course + double dnc; // closest distance to course from next to next to next wp + double aalt; // assigned altitude + double ahdg; // assigned heading + bool on_crs; // is the plane on course? + bool wp_change; // way point has changed + double tlm; // time when last message was sent + TransCode lmc; // code of last message +}; + + +class FGApproach : public FGATC { + + int bucket; + + string active_runway; + double active_rw_hdg; + double active_rw_lon; + double active_rw_lat; + double active_rw_len; + + int num_planes; // number of planes on the stack + PlaneApp planes[max_planes]; // Array of planes + string transmission; + bool first; + + SGPropertyNode_ptr comm1_node; + SGPropertyNode_ptr comm2_node; + + SGPropertyNode_ptr atcmenu_node; + SGPropertyNode_ptr atcopt0_node; + SGPropertyNode_ptr atcopt1_node; + SGPropertyNode_ptr atcopt2_node; + SGPropertyNode_ptr atcopt3_node; + SGPropertyNode_ptr atcopt4_node; + SGPropertyNode_ptr atcopt5_node; + SGPropertyNode_ptr atcopt6_node; + SGPropertyNode_ptr atcopt7_node; + SGPropertyNode_ptr atcopt8_node; + SGPropertyNode_ptr atcopt9_node; + + // for failure modeling + string trans_ident; // transmitted ident + bool approach_failed; // approach failed? + +public: + + FGApproach(void); + ~FGApproach(void); + + void Init(); + + void Update(double dt); + + // Add new plane to stack if not already registered + // Input: pid - id of plane (name) + // Output: "true" if added; "false" if already existend + void AddPlane(const string& pid); + + // Remove plane from stack if out of range + int RemovePlane(); + + inline double get_bucket() const { return bucket; } + inline int get_pnum() const { return num_planes; } + inline const string& get_trans_ident() { return trans_ident; } + +private: + + void calc_wp( const int &i); + + void update_plane_dat(); + + void get_active_runway(); + + void update_param(const int &i); + + double round_alt( bool hl, double alt ); + + double angle_diff_deg( const double &a1, const double &a2); + + void set_message(const string &s); + +// ======================================================================== +// get point2 given starting point1 and course and distance +// input: point1 = heading in degrees, distance +// input: course in degrees, distance +// output: point2 = heading in degrees, distance +// ======================================================================== + void calc_cd_head_dist(const double &h1, const double &d1, + const double &course, const double &dist, + double *h2, double *d2); + + +// ======================================================================== +// get heading and distance between two points; point2 ---> point1 +// input: point1 = heading in degrees, distance +// input: point2 = heading in degrees, distance +// output: course in degrees, distance +// ======================================================================== + void calc_hd_course_dist(const double &h1, const double &d1, + const double &h2, const double &d2, + double *course, double *dist); + + + +// ======================================================================== +// closest distance between a point and a straigt line in 2 dim. +// the input variables are given in (heading, distance) +// relative to a common point +// input: point = heading in degrees, distance +// input: straigt line = anker vector (heading in degrees, distance), +// heading of direction vector +// output: distance +// ======================================================================== + double calc_psl_dist(const double &h1, const double &d1, + const double &h2, const double &d2, + const double &h3); + + // Pointers to current users position + SGPropertyNode_ptr lon_node; + SGPropertyNode_ptr lat_node; + SGPropertyNode_ptr elev_node; + SGPropertyNode_ptr hdg_node; + SGPropertyNode_ptr speed_node; + SGPropertyNode_ptr etime_node; + + //Update the transmission string + void UpdateTransmission(void); + + friend istream& operator>> ( istream&, FGApproach& ); +}; + +#endif // _FG_APPROACH_HXX diff --git a/src/ATCDCL/atis.cxx b/src/ATCDCL/atis.cxx new file mode 100644 index 000000000..0957d18ac --- /dev/null +++ b/src/ATCDCL/atis.cxx @@ -0,0 +1,225 @@ +// atis.cxx - routines to generate the ATIS info string +// This is the implementation of the FGATIS class +// +// Written by David Luff, started October 2001. +// +// Copyright (C) 2001 David C Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/compiler.h> + +#include <stdlib.h> // atoi() +#include <stdio.h> // sprintf +#include <string> +SG_USING_STD(string); + +#include STL_IOSTREAM +SG_USING_STD(cout); + +#include <simgear/misc/sg_path.hxx> + +#include <Environment/environment_mgr.hxx> +#include <Environment/environment.hxx> + +#include <Main/fg_props.hxx> +#include <Main/globals.hxx> +#include <Airports/runways.hxx> + +#include "atis.hxx" +#include "commlist.hxx" +#include "ATCutils.hxx" +#include "ATCmgr.hxx" + +FGATIS::FGATIS() : + transmission(""), + trans_ident(""), + atis_failed(false), + refname("atis") + //type(ATIS) +{ + _vPtr = globals->get_ATC_mgr()->GetVoicePointer(ATIS); + _voiceOK = (_vPtr == NULL ? false : true); + _type = ATIS; +} + +FGATIS::~FGATIS() { +} + +// Main update function - checks whether we are displaying or not the correct message. +void FGATIS::Update(double dt) { + if(_display) { + if(_displaying) { + // Check if we need to update the message + // - basically every hour and if the weather changes significantly at the station + } else { + // We need to get and display the message + UpdateTransmission(); + //cout << "ATIS.CXX - calling ATCMgr to render transmission..." << endl; + Render(transmission, refname, true); + _displaying = true; + } + } else { + // We shouldn't be displaying + if(_displaying) { + //cout << "ATIS.CXX - calling NoRender()..." << endl; + NoRender(refname); + _displaying = false; + } + } +} + +// Sets the actual broadcast ATIS transmission. +void FGATIS::UpdateTransmission() { + double visibility; + char buf[10]; + int phonetic_id; + string phonetic_id_string; + string time_str = fgGetString("sim/time/gmt-string"); + int hours; + // int minutes; + + FGEnvironment stationweather = + ((FGEnvironmentMgr *)globals->get_subsystem("environment")) + ->getEnvironment(lat, lon, 0.0); + + transmission = ""; + + // UK CAA radiotelephony manual indicated ATIS transmissions start with "This is" + // Not sure about rest of the world though. + transmission += "This_is "; + // transmitted station name. + transmission += name; + // Add "Information" + transmission += " information"; + + //cout << "In atis.cxx, time_str = " << time_str << '\n'; + // Add the recording identifier + // For now we will assume we only transmit every hour + hours = atoi((time_str.substr(1,2)).c_str()); //Warning - this is fragile if the + //time string format changes + //cout << "In atis.cxx, hours = " << hours << endl; + phonetic_id = current_commlist->GetCallSign(ident, hours, 0); + phonetic_id_string = GetPhoneticIdent(phonetic_id); + transmission += " "; + transmission += phonetic_id_string; + + // Output the recording time. - we'll just output the last whole hour for now. + // FIXME - this only gets GMT time but that appears to be all the clock outputs for now + //cout << "in atis.cxx, time = " << time_str << endl; + transmission = transmission + " / Weather " + ConvertNumToSpokenDigits((time_str.substr(0,3) + "00")) + " hours zulu"; + + // Get the temperature + int temp; + temp = (int)stationweather.get_temperature_degc(); + + // HACK ALERT - at the moment the new environment subsystem returns bogus temperatures + // FIXME - take out this hack when this gets fixed upstream + if((temp < -50) || (temp > 60)) { + temp = 15; + } + + sprintf(buf, "%i", abs(temp)); + transmission += " / Temperature "; + if(temp < 0) { + transmission += "minus "; + } + string tempstr1 = buf; + string tempstr2; + transmission += ConvertNumToSpokenDigits(tempstr1); + transmission += " degrees_Celsius"; + + // Get the visibility + visibility = stationweather.get_visibility_m(); + sprintf(buf, "%i", int(visibility/1600)); + transmission += " / Visibility "; + tempstr1 = buf; + transmission += ConvertNumToSpokenDigits(tempstr1); + transmission += " miles"; + + // Get the cloudbase + // FIXME: kludge for now + if (strcmp(fgGetString("/environment/clouds/layer[0]/type"), "clear")) { + double cloudbase = + fgGetDouble("/environment/clouds/layer[0]/elevation-ft"); + // For some reason the altitude returned doesn't seem to correspond to the actual cloud altitude. + char buf3[10]; + char buf4[10]; + // cout << "cloudbase = " << cloudbase << endl; + sprintf(buf3, "%i", int(cloudbase)/1000); + sprintf(buf4, "%i", ((int)cloudbase % 1000)/100); + transmission += " / Cloudbase"; + if(int(cloudbase)/1000) { + tempstr1 = buf3; + transmission = transmission + " " + ConvertNumToSpokenDigits(tempstr1) + " thousand"; + } + if(((int)cloudbase % 1000)/100) { + tempstr1 = buf4; + transmission = transmission + " " + ConvertNumToSpokenDigits(tempstr1) + " hundred"; + } + transmission += " feet"; + } + + // Get the pressure / altimeter + double P = fgGetDouble("/environment/pressure-sea-level-inhg"); + if(ident.substr(0,2) == "EG" && fgGetBool("/sim/atc/use-millibars") == true) { + // Convert to millibars for the UK! + P *= 33.864; + sprintf(buf, "%.0f", P); + } else { + sprintf(buf, "%.2f", P); + } + transmission += " / Altimeter "; + tempstr1 = buf; + transmission += ConvertNumToSpokenDigits(tempstr1); + + // Based on the airport-id and wind get the active runway + //FGRunway *r; + double speed = stationweather.get_wind_speed_kt(); + double hdg = stationweather.get_wind_from_heading_deg(); + if (speed == 0) { + hdg = 270; // This forces West-facing rwys to be used in no-wind situations + // which is consistent with Flightgear's initial setup. + transmission += " / Winds_light_and_variable"; + } else { + // FIXME: get gust factor in somehow + char buf5[10]; + char buf6[10]; + sprintf(buf5, "%i", int(speed)); + sprintf(buf6, "%i", int(hdg)); + tempstr1 = buf5; + tempstr2 = buf6; + transmission = transmission + " / Winds " + ConvertNumToSpokenDigits(tempstr1) + " knots from " + + ConvertNumToSpokenDigits(tempstr2) + " degrees"; + } + + string rwy_no = globals->get_runways()->search(ident, int(hdg)); + if(rwy_no != "NN") { + transmission += " / Landing_and_departing_runway "; + transmission += ConvertRwyNumToSpokenString(atoi(rwy_no.c_str())); + //cout << "in atis.cxx, r.rwy_no = " << rwy_no << " r.id = " << r->id << " r.heading = " << r->heading << endl; + } + + // Anything else? + + transmission += " / Advise_controller_on_initial_contact_you_have "; + transmission += phonetic_id_string; + transmission += " /// "; +} diff --git a/src/ATCDCL/atis.hxx b/src/ATCDCL/atis.hxx new file mode 100644 index 000000000..05835d4a7 --- /dev/null +++ b/src/ATCDCL/atis.hxx @@ -0,0 +1,95 @@ +// atis.hxx -- ATIS class +// +// Written by David Luff, started October 2001. +// Based on nav.hxx by Curtis Olson, started April 2000. +// +// Copyright (C) 2001 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#ifndef _FG_ATIS_HXX +#define _FG_ATIS_HXX + +#include <stdio.h> +#include <string> + +#include <simgear/compiler.h> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/magvar/magvar.hxx> +#include <simgear/timing/sg_time.hxx> + +#ifdef SG_HAVE_STD_INCLUDES +# include <istream> +# include <iomanip> +#elif defined( __BORLANDC__ ) || (__APPLE__) +# include <iostream> +#else +# include <istream.h> +# include <iomanip.h> +#endif + +SG_USING_STD(istream); +SG_USING_STD(string); + +#include "ATC.hxx" + +//DCL - a complete guess for now. +#define FG_ATIS_DEFAULT_RANGE 30 + +class FGATIS : public FGATC { + + //atc_type type; + string transmission; // The actual ATIS transmission + // This is not stored in default.atis but is generated + // from the prevailing conditions when required. + + // for failure modeling + string trans_ident; // transmitted ident + bool atis_failed; // atis failed? + + // Aircraft position + // ATIS is actually a special case in that unlike other ATC eg.tower it doesn't actually know about + // or the whereabouts of the aircraft it is transmitting to. However, to ensure consistancy of + // operation with the other ATC classes the ATIS class must calculate range to the aircraft in order + // to decide whether to render the transmission - hence the users plane details must be stored. + //SGPropertyNode_ptr airplane_lon_node; + //SGPropertyNode_ptr airplane_lat_node; + //SGPropertyNode_ptr airplane_elev_node; + + public: + + FGATIS(void); + ~FGATIS(void); + + //run the ATIS instance + void Update(double dt); + + //inline void set_type(const atc_type tp) {type = tp;} + inline const string& get_trans_ident() { return trans_ident; } + inline void set_refname(const string& r) { refname = r; } + + private: + + string refname; // Holds the refname of a transmission in progress + + //Update the transmission string + void UpdateTransmission(void); + + friend istream& operator>> ( istream&, FGATIS& ); +}; + +#endif // _FG_ATIS_HXX diff --git a/src/ATCDCL/commlist.cxx b/src/ATCDCL/commlist.cxx new file mode 100644 index 000000000..4d27e1c79 --- /dev/null +++ b/src/ATCDCL/commlist.cxx @@ -0,0 +1,346 @@ +// commlist.cxx -- comm frequency lookup class +// +// Written by David Luff and Alexander Kappes, started Jan 2003. +// Based on navlist.cxx by Curtis Olson, started April 2000. +// +// Copyright (C) 2000 Curtis L. Olson - http://www.flightgear.org/~curt +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/debug/logstream.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/math/sg_random.h> +#include <simgear/bucket/newbucket.hxx> +#include <Airports/simple.hxx> + +#include "commlist.hxx" +//#include "atislist.hxx" +#include "ATCutils.hxx" + + +FGCommList *current_commlist; + + +// Constructor +FGCommList::FGCommList( void ) { +} + + +// Destructor +FGCommList::~FGCommList( void ) { +} + + +// load the navaids and build the map +bool FGCommList::init( const SGPath& path ) { + + SGPath temp = path; + commlist_freq.erase(commlist_freq.begin(), commlist_freq.end()); + commlist_bck.erase(commlist_bck.begin(), commlist_bck.end()); + temp.append( "ATC/default.atis" ); + LoadComms(temp); + temp = path; + temp.append( "ATC/default.tower" ); + LoadComms(temp); + temp = path; + temp.append( "ATC/default.ground" ); + LoadComms(temp); + temp = path; + temp.append( "ATC/default.approach" ); + LoadComms(temp); + return true; +} + + +bool FGCommList::LoadComms(const SGPath& path) { + + sg_gzifstream fin( path.str() ); + if ( !fin.is_open() ) { + SG_LOG( SG_GENERAL, SG_ALERT, "Cannot open file: " << path.str() ); + exit(-1); + } + + // read in each line of the file + fin >> skipcomment; + +#ifdef __MWERKS__ + char c = 0; + while ( fin.get(c) && c != '\0' ) { + fin.putback(c); +#else + while ( !fin.eof() ) { +#endif + ATCData a; + fin >> a; + if(a.type == INVALID) { + SG_LOG(SG_GENERAL, SG_DEBUG, "WARNING - INVALID type found in " << path.str() << '\n'); + } else { + // Push all stations onto frequency map + commlist_freq[a.freq].push_back(a); + + // Push non-atis stations onto bucket map as well + // In fact, push all stations onto bucket map for now so FGATCMgr::GetFrequency() works. + //if(a.type != ATIS) { + // get bucket number + SGBucket bucket(a.lon, a.lat); + int bucknum = bucket.gen_index(); + commlist_bck[bucknum].push_back(a); + //} + } + + fin >> skipcomment; + } + + fin.close(); + return true; +} + + +// query the database for the specified frequency, lon and lat are in +// degrees, elev is in meters +// If no atc_type is specified, it returns true if any non-invalid type is found +// If atc_type is specifed, returns true only if the specified type is found +bool FGCommList::FindByFreq( double lon, double lat, double elev, double freq, + ATCData* ad, atc_type tp ) +{ + lon *= SGD_DEGREES_TO_RADIANS; + lat *= SGD_DEGREES_TO_RADIANS; + + // HACK - if freq > 1000 assume it's in KHz, otherwise assume MHz. + // A bit ugly but it works for now!!!! + comm_list_type stations; + if(freq > 1000.0) { + stations = commlist_freq[(int)freq]; + } else { + stations = commlist_freq[(int)(freq*100.0 + 0.5)]; + } + comm_list_iterator current = stations.begin(); + comm_list_iterator last = stations.end(); + + // double az1, az2, s; + Point3D aircraft = sgGeodToCart( Point3D(lon, lat, elev) ); + Point3D station; + const double orig_max_d = 1e100; + double max_d = orig_max_d; + double d; + // TODO - at the moment this loop returns the first match found in range + // We want to return the closest match in the event of a frequency conflict + for ( ; current != last ; ++current ) { + //cout << "testing " << current->get_ident() << endl; + station = Point3D(current->x, current->y, current->z); + //cout << "aircraft = " << aircraft << endl; + //cout << "station = " << station << endl; + + d = aircraft.distance3Dsquared( station ); + + //cout << " dist = " << sqrt(d) + // << " range = " << current->range * SG_NM_TO_METER << endl; + + // TODO - match up to twice the published range so we can model + // reduced signal strength + // NOTE The below is squared since we match to distance3Dsquared (above) to avoid a sqrt. + if ( d < (current->range * SG_NM_TO_METER + * current->range * SG_NM_TO_METER ) ) { + //cout << "matched = " << current->ident << endl; + if((tp == INVALID) || (tp == (*current).type)) { + if(d < max_d) { + max_d = d; + *ad = *current; + } + } + } + } + + if(max_d < orig_max_d) { + return true; + } else { + return false; + } +} + +int FGCommList::FindByPos(double lon, double lat, double elev, double range, comm_list_type* stations, atc_type tp) +{ + // number of relevant stations found within range + int found = 0; + stations->erase(stations->begin(), stations->end()); + + // get bucket number for plane position + SGBucket buck(lon, lat); + + // get neigboring buckets + int bx = (int)( range*SG_NM_TO_METER / buck.get_width_m() / 2); + int by = (int)( range*SG_NM_TO_METER / buck.get_height_m() / 2 ); + + // loop over bucket range + for ( int i=-bx; i<=bx; i++) { + for ( int j=-by; j<=by; j++) { + buck = sgBucketOffset(lon, lat, i, j); + long int bucket = buck.gen_index(); + comm_list_type Fstations = commlist_bck[bucket]; + comm_list_iterator current = Fstations.begin(); + comm_list_iterator last = Fstations.end(); + + double rlon = lon * SGD_DEGREES_TO_RADIANS; + double rlat = lat * SGD_DEGREES_TO_RADIANS; + + // double az1, az2, s; + Point3D aircraft = sgGeodToCart( Point3D(rlon, rlat, elev) ); + Point3D station; + double d; + for(; current != last; ++current) { + if((current->type == tp) || (tp == INVALID)) { + station = Point3D(current->x, current->y, current->z); + d = aircraft.distance3Dsquared( station ); + // NOTE The below is squared since we match to distance3Dsquared (above) to avoid a sqrt. + if ( d < (current->range * SG_NM_TO_METER + * current->range * SG_NM_TO_METER ) ) { + stations->push_back(*current); + ++found; + } + } + } + } + } + return found; +} + + +// Returns the distance in meters to the closest station of a given type, +// with the details written into ATCData& ad. If no type is specifed simply +// returns the distance to the closest station of any type. +// Returns -9999 if no stations found within max_range in nautical miles (default 100 miles). +// Note that the search algorithm starts at 10 miles and multiplies by 10 thereafter, so if +// say 300 miles is specifed 10, then 100, then 1000 will be searched, breaking at first result +// and giving up after 1000. +double FGCommList::FindClosest( double lon, double lat, double elev, ATCData& ad, atc_type tp, double max_range) { + int num_stations = 0; + int range = 10; + comm_list_type stations; + comm_list_iterator itr; + double distance = -9999.0; + + while(num_stations == 0) { + num_stations = FindByPos(lon, lat, elev, range, &stations, tp); + if(num_stations) { + double closest = max_range * SG_NM_TO_METER; + double tmp; + for(itr = stations.begin(); itr != stations.end(); ++itr) { + ATCData ad2 = *itr; + //Point3D p1(*itr.lon, *itr.lat, *itr.elev); + Point3D p1(ad2.lon, ad2.lat, ad2.elev); + const FGAirport *a = fgFindAirportID(ad2.ident); + if (a) { + Point3D p2(lon, lat, elev); + tmp = dclGetHorizontalSeparation(p1, p2); + if(tmp <= closest) { + closest = tmp; + distance = tmp; + ad = *itr; + } + } + } + //cout << "Closest station is " << ad.ident << " at a range of " << distance << " meters\n"; + return(distance); + } + if(range > max_range) { + break; + } + range *= 10; + } + return(-9999.0); +} + + +// Find by Airport code. +// This is basically a wrapper for a call to the airport database to get the airport +// position followed by a call to FindByPos(...) +bool FGCommList::FindByCode( const string& ICAO, ATCData& ad, atc_type tp ) { + const FGAirport *a = fgFindAirportID( ICAO); + if ( a) { + comm_list_type stations; + int found = FindByPos(a->getLongitude(), a->getLatitude(), a->getElevation(), 10.0, &stations, tp); + if(found) { + comm_list_iterator itr = stations.begin(); + while(itr != stations.end()) { + if(((*itr).ident == ICAO) && ((*itr).type == tp)) { + ad = *itr; + return true; + } + ++itr; + } + } + } else { + return false; + } + return false; +} + + +// TODO - this function should move somewhere else eventually! +// Return an appropriate call-sign for an ATIS transmission. +int FGCommList::GetCallSign( const string& apt_id, int hours, int mins ) +{ + atis_transmission_type tran; + + if(atislog.find(apt_id) == atislog.end()) { + // This station has not transmitted yet - return a random identifier + // and add the transmission to the log + tran.hours = hours; + tran.mins = mins; + sg_srandom_time(); + tran.callsign = int(sg_random() * 25) + 1; // This *should* give a random int between 1 and 26 + //atislog[apt_id].push_back(tran); + atislog[apt_id] = tran; + } else { + // This station has transmitted - calculate the appropriate identifier + // and add the transmission to the log if it has changed + tran = atislog[apt_id]; + // This next bit assumes that no-one comes back to the same ATIS station + // after running FlightGear for more than 24 hours !! + if((tran.hours == hours) && (tran.mins == mins)) { + return(tran.callsign); + } else { + if(tran.hours == hours) { + // The minutes must have changed + tran.mins = mins; + tran.callsign++; + } else { + if(hours < tran.hours) { + hours += 24; + } + tran.callsign += (hours - tran.hours); + if(mins != 0) { + // Assume transmissions were made on every hour + tran.callsign++; + } + tran.hours = hours; + tran.mins = mins; + } + // Wrap if we've exceeded Zulu + if(tran.callsign > 26) { + tran.callsign -= 26; + } + // And write the new transmission to the log + atislog[apt_id] = tran; + } + } + return(tran.callsign); +} diff --git a/src/ATCDCL/commlist.hxx b/src/ATCDCL/commlist.hxx new file mode 100644 index 000000000..1cb2f4855 --- /dev/null +++ b/src/ATCDCL/commlist.hxx @@ -0,0 +1,140 @@ +// commlist.hxx -- comm frequency lookup class +// +// Written by David Luff and Alexander Kappes, started Jan 2003. +// Based on navlist.hxx by Curtis Olson, started April 2000. +// +// Copyright (C) 2000 Curtis L. Olson - http://www.flightgear.org/~curt +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +/***************************************************************** +* +* FGCommList is used to store communication frequency information +* for the ATC and AI subsystems. Two maps are maintained - one +* searchable by location and one searchable by frequency. The +* data structure returned from the search is the ATCData struct +* defined in ATC.hxx, containing location, frequency, name, range +* and type of the returned station. +* +******************************************************************/ + +#ifndef _FG_COMMLIST_HXX +#define _FG_COMMLIST_HXX + + +#include <simgear/compiler.h> +#include <simgear/misc/sg_path.hxx> + +#include <map> +#include <list> +#include <string> + +#include "ATC.hxx" +#include "atis.hxx" + +SG_USING_STD(list); +SG_USING_STD(map); +SG_USING_STD(vector); +SG_USING_STD(string); + +// A list of ATC stations +typedef list < ATCData > comm_list_type; +typedef comm_list_type::iterator comm_list_iterator; +typedef comm_list_type::const_iterator comm_list_const_iterator; + +// A map of ATC station lists +typedef map < int, comm_list_type > comm_map_type; +typedef comm_map_type::iterator comm_map_iterator; +typedef comm_map_type::const_iterator comm_map_const_iterator; + + +class FGCommList { + +public: + + FGCommList(); + ~FGCommList(); + + // load all comm frequencies and build the map + bool init( const SGPath& path ); + + // query the database for the specified frequency, lon and lat are + // in degrees, elev is in meters. + // If no atc_type is specified, it returns true if any non-invalid type is found. + // If atc_type is specifed, returns true only if the specified type is found. + // Returns the station closest to the supplied position. + // The data found is written into the passed-in ATCData structure. + bool FindByFreq( double lon, double lat, double elev, double freq, ATCData* ad, atc_type tp = INVALID ); + + // query the database by location, lon and lat are in degrees, elev is in meters, range is in nautical miles. + // Returns the number of stations of the specified atc_type tp that are in range of the position defined by + // lon, lat and elev, and pushes them into stations. + // If no atc_type is specifed, returns the number of all stations in range, and pushes them into stations + // ** stations is erased before use ** + int FindByPos( double lon, double lat, double elev, double range, comm_list_type* stations, atc_type tp = INVALID ); + + // Returns the distance in meters to the closest station of a given type, + // with the details written into ATCData& ad. If no type is specifed simply + // returns the distance to the closest station of any type. + // Returns -9999 if no stations found within max_range in nautical miles (default 100 miles). + // Note that the search algorithm starts at 10 miles and multiplies by 10 thereafter, so if + // say 300 miles is specifed 10, then 100, then 1000 will be searched, breaking at first result + // and giving up after 1000. + // !!!Be warned that searching anything over 100 miles will pause the sim unacceptably!!! + // (The ability to search longer ranges should be used during init only). + double FindClosest( double lon, double lat, double elev, ATCData& ad, atc_type tp = INVALID, double max_range = 100.0 ); + + // Find by Airport code. + bool FindByCode( const string& ICAO, ATCData& ad, atc_type tp = INVALID ); + + // Return the callsign for an ATIS transmission given transmission time and airport id + // This maybe should get moved somewhere else!! + int GetCallSign( const string& apt_id, int hours, int mins ); + +private: + + // Comm stations mapped by frequency + comm_map_type commlist_freq; + + // Comm stations mapped by bucket + comm_map_type commlist_bck; + + // Load comms from a specified path (which must include the filename) + bool LoadComms(const SGPath& path); + +//----------- This stuff is left over from atislist.[ch]xx and maybe should move somewhere else + // Add structure and map for storing a log of atis transmissions + // made in this session of FlightGear. This allows the callsign + // to be allocated correctly wrt time. + typedef struct { + int hours; + int mins; + int callsign; + } atis_transmission_type; + + typedef map < string, atis_transmission_type > atis_log_type; + typedef atis_log_type::iterator atis_log_iterator; + typedef atis_log_type::const_iterator atis_log_const_iterator; + + atis_log_type atislog; +//----------------------------------------------------------------------------------------------- + +}; + + +extern FGCommList *current_commlist; + + +#endif // _FG_COMMLIST_HXX diff --git a/src/ATCDCL/ground.cxx b/src/ATCDCL/ground.cxx new file mode 100644 index 000000000..4adfa9958 --- /dev/null +++ b/src/ATCDCL/ground.cxx @@ -0,0 +1,732 @@ +// FGGround - a class to provide ground control at larger airports. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include <simgear/misc/sg_path.hxx> +#include <simgear/math/sg_random.h> +#include <simgear/debug/logstream.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/constants.h> +#include <Main/globals.hxx> + +#include <stdlib.h> +#include STL_FSTREAM + +#include "ground.hxx" +#include "ATCutils.hxx" +#include "AILocalTraffic.hxx" +#include "ATCmgr.hxx" + +SG_USING_STD(ifstream); +SG_USING_STD(cout); + +node::node() { +} + +node::~node() { + for(unsigned int i=0; i < arcs.size(); ++i) { + delete arcs[i]; + } +} + +// Make sure that a_path.cost += distance is safe from the moment it's created. +a_path::a_path() { + cost = 0; +} + +FGGround::FGGround() { + ATCmgr = globals->get_ATC_mgr(); + _type = GROUND; + networkLoadOK = false; + ground_traffic.erase(ground_traffic.begin(), ground_traffic.end()); + ground_traffic_itr = ground_traffic.begin(); + + // Init the property nodes - TODO - need to make sure we're getting surface winds. + wind_from_hdg = fgGetNode("/environment/wind-from-heading-deg", true); + wind_speed_knots = fgGetNode("/environment/wind-speed-kt", true); + + // TODO - get the actual airport elevation + aptElev = 0.0; +} + +FGGround::FGGround(const string& id) { + ATCmgr = globals->get_ATC_mgr(); + networkLoadOK = false; + ground_traffic.erase(ground_traffic.begin(), ground_traffic.end()); + ground_traffic_itr = ground_traffic.begin(); + ident = id; + + // Init the property nodes - TODO - need to make sure we're getting surface winds. + wind_from_hdg = fgGetNode("/environment/wind-from-heading-deg", true); + wind_speed_knots = fgGetNode("/environment/wind-speed-kt", true); + + // TODO - get the actual airport elevation + aptElev = 0.0; +} + +FGGround::~FGGround() { +} + +void FGGround::ParseRwyExits(node* np, char* es) { + char* token; + char estr[20]; + strcpy(estr, es); + const char delimiters[] = "-"; + token = strtok(estr, delimiters); + while(token != NULL) { + int i = atoi(token); + //cout << "token = " << token << endl; + //cout << "rwy number = " << i << endl; + //runways[(atoi(token))].exits.push_back(np); + runways[i].exits.push_back(np); + //cout << "token = " << token << '\n'; + token = strtok(NULL, delimiters); + } +} + + +// Load the ground logical network of the current instances airport +// Return true if successfull. +// TODO - currently the file is assumed to reside in the base/ATC directory. +// This might change to something more thought out in the future. +// NOTE - currently it is assumed that all nodes are loaded before any arcs. +// It won't work ATM if this doesn't hold true. +bool FGGround::LoadNetwork() { + node* np; + arc* ap; + Gate* gp; + + int gateCount = 0; // This is used to allocate gateID's from zero upwards + // This may well change in the future - probably to reading in the real-world + // gate numbers from file. + + ifstream fin; + SGPath path = globals->get_fg_root(); + //string taxiPath = "ATC/" + ident + ".taxi"; + string taxiPath = "ATC/KEMT.taxi"; // FIXME - HARDWIRED FOR TESTING + path.append(taxiPath); + + SG_LOG(SG_ATC, SG_INFO, "Trying to read taxiway data for " << ident << "..."); + //cout << "Trying to read taxiway data for " << ident << "..." << endl; + fin.open(path.c_str(), ios::in); + if(!fin) { + SG_LOG(SG_ATC, SG_ALERT, "Unable to open taxiway data input file " << path.c_str()); + //cout << "Unable to open taxiway data input file " << path.c_str() << endl; + return(false); + } + + char ch; + char buf[30]; + while(!fin.eof()) { + fin >> buf; + // Node, arc, or [End]? + //cout << "Read in ground network element type = " << buf << endl; + if(!strcmp(buf, "[End]")) { // TODO - maybe make this more robust to spelling errors by just looking for '[' + SG_LOG(SG_ATC, SG_INFO, "Done reading " << path.c_str() << endl); + break; + } else if(!strcmp(buf, "N")) { + // Node + np = new node; + np->struct_type = NODE; + fin >> buf; + np->nodeID = atoi(buf); + fin >> buf; + np->pos.setlon(atof(buf)); + fin >> buf; + np->pos.setlat(atof(buf)); + fin >> buf; + np->pos.setelev(atof(buf)); + fin >> buf; // node type + if(!strcmp(buf, "J")) { + np->type = JUNCTION; + } else if(!strcmp(buf, "T")) { + np->type = TJUNCTION; + } else if(!strcmp(buf, "H")) { + np->type = HOLD; + } else { + SG_LOG(SG_ATC, SG_ALERT, "**** ERROR ***** Unknown node type in taxi network...\n"); + delete np; + return(false); + } + fin >> buf; // rwy exit information - gets parsed later - FRAGILE - will break if buf is reused. + // Now the name + fin >> ch; // strip the leading " off + np->name = ""; + while(1) { + fin.unsetf(ios::skipws); + fin >> ch; + if((ch == '"') || (ch == 0x0A)) { + break; + } // we shouldn't need the 0x0A but it makes a nice safely in case someone leaves off the " + np->name += ch; + } + fin.setf(ios::skipws); + network.push_back(np); + // FIXME - fragile - replies on buf not getting modified from exits read to here + // see if we also need to push it onto the runway exit list + //cout << "strlen(buf) = " << strlen(buf) << endl; + if(strlen(buf) > 2) { + //cout << "Calling ParseRwyExits for " << buf << endl; + ParseRwyExits(np, buf); + } + } else if(!strcmp(buf, "A")) { + ap = new arc; + ap->struct_type = ARC; + fin >> buf; + ap->n1 = atoi(buf); + fin >> buf; + ap->n2 = atoi(buf); + fin >> buf; + if(!strcmp(buf, "R")) { + ap->type = RUNWAY; + } else if(!strcmp(buf, "T")) { + ap->type = TAXIWAY; + } else { + SG_LOG(SG_ATC, SG_ALERT, "**** ERROR ***** Unknown arc type in taxi network...\n"); + delete ap; + return(false); + } + // directed? + fin >> buf; + if(!strcmp(buf, "Y")) { + ap->directed = true; + } else if(!strcmp(buf, "N")) { + ap->directed = false; + } else { + SG_LOG(SG_ATC, SG_ALERT, "**** ERROR ***** Unknown arc directed value in taxi network - should be Y/N !!!\n"); + delete ap; + return(false); + } + // Now the name + ap->name = ""; + while(1) { + fin.unsetf(ios::skipws); + fin >> ch; + ap->name += ch; + if((ch == '"') || (ch == 0x0A)) { + break; + } // we shouldn't need the 0x0A but it makes a nice safely in case someone leaves off the " + } + fin.setf(ios::skipws); + ap->distance = (int)dclGetHorizontalSeparation(network[ap->n1]->pos, network[ap->n2]->pos); + //cout << "Distance = " << ap->distance << '\n'; + network[ap->n1]->arcs.push_back(ap); + network[ap->n2]->arcs.push_back(ap); + } else if(!strcmp(buf, "G")) { + gp = new Gate; + gp->struct_type = NODE; + gp->type = GATE; + fin >> buf; + gp->nodeID = atoi(buf); + fin >> buf; + gp->pos.setlon(atof(buf)); + fin >> buf; + gp->pos.setlat(atof(buf)); + fin >> buf; + gp->pos.setelev(atof(buf)); + fin >> buf; // gate type - ignore this for now + fin >> buf; // gate heading + gp->heading = atoi(buf); + // Now the name + gp->name = ""; + while(1) { + fin.unsetf(ios::skipws); + fin >> ch; + gp->name += ch; + if((ch == '"') || (ch == 0x0A)) { + break; + } // we shouldn't need the 0x0A but it makes a nice safely in case someone leaves off the " + } + fin.setf(ios::skipws); + gp->id = gateCount; // Warning - this will likely change in the future. + gp->used = false; + network.push_back(gp); + gates[gateCount] = gp; + gateCount++; + } else { + // Something has gone seriously pear-shaped + SG_LOG(SG_ATC, SG_ALERT, "********* ERROR - unknown ground network element type... aborting read of " << path.c_str() << '\n'); + return(false); + } + + fin >> skipeol; + } + return(true); +} + +void FGGround::Init() { + untowered = false; + + // Figure out which is the active runway - TODO - it would be better to have ground call tower + // for runway operation details, but at the moment we can't guarantee that tower control at a given airport + // will be initialised before ground so we can't do that. + DoRwyDetails(); + //cout << "In FGGround::Init, active rwy is " << activeRwy << '\n'; + ortho.Init(rwy.threshold_pos, rwy.hdg); + + networkLoadOK = LoadNetwork(); +} + +void FGGround::Update(double dt) { + // Each time step, what do we need to do? + // We need to go through the list of outstanding requests and acknowedgements + // and process at least one of them. + // We need to go through the list of planes under our control and check if + // any need to be addressed. + // We need to check for planes not under our control coming within our + // control area and address if necessary. + + // Lets take the example of a plane which has just contacted ground + // following landing - presumably requesting where to go? + // First we need to establish the position of the plane within the logical network. + // Next we need to decide where its going. + + if(ground_traffic.size()) { + if(ground_traffic_itr == ground_traffic.end()) { + ground_traffic_itr = ground_traffic.begin(); + } + + //Process(*ground_traffic_itr); + GroundRec* g = *ground_traffic_itr; + if(g->taxiRequestOutstanding) { + double responseTime = 10.0; // seconds - this should get more sophisticated at some point + if(g->clearanceCounter > responseTime) { + // DO CLEARANCE + // TODO - move the mechanics of making up the transmission out of the main Update(...) routine. + string trns = ""; + trns += g->plane.callsign; + trns += " taxi holding point runway "; // TODO - add the holding point name + // eg " taxi holding point G2 runway " + trns += ConvertRwyNumToSpokenString(activeRwy); + if(_display) { + fgSetString("/sim/messages/ground", trns.c_str()); + } + g->planePtr->RegisterTransmission(1); // cleared to taxi + g->clearanceCounter = 0.0; + g->taxiRequestOutstanding = false; + } else { + g->clearanceCounter += (dt * ground_traffic.size()); + } + } else if(((FGAILocalTraffic*)(g->planePtr))->AtHoldShort()) { // That's a hack - eventually we should monitor actual position + // HACK ALERT - the automatic cast to AILocalTraffic has to go once we have other sorts working!!!!! FIXME TODO + // NOTE - we don't need to do the contact tower bit unless we have separate tower and ground + string trns = g->plane.callsign; + trns += " contact Tower "; + double f = globals->get_ATC_mgr()->GetFrequency(ident, TOWER) / 100.0; + char buf[10]; + sprintf(buf, "%.2f", f); + trns += buf; + if(_display) { + fgSetString("/sim/messages/ground", trns.c_str()); + } + g->planePtr->RegisterTransmission(2); // contact tower + delete *ground_traffic_itr; + ground_traffic.erase(ground_traffic_itr); + ground_traffic_itr = ground_traffic.begin(); + } + ++ground_traffic_itr; + } + + // Call the base class update for the response time handling. + FGATC::Update(dt); +} + +// Figure out which runways are active. +// For now we'll just be simple and do one active runway - eventually this will get much more complex +// Copied from FGTower - TODO - it would be better to implement this just once, and have ground call tower +// for runway operation details, but at the moment we can't guarantee that tower control at a given airport +// will be initialised before ground so we can't do that. +void FGGround::DoRwyDetails() { + //cout << "GetRwyDetails called" << endl; + + // Based on the airport-id and wind get the active runway + + //wind + double hdg = wind_from_hdg->getDoubleValue(); + double speed = wind_speed_knots->getDoubleValue(); + hdg = (speed == 0.0 ? 270.0 : hdg); + //cout << "Heading = " << hdg << '\n'; + + FGRunway runway; + bool rwyGood = globals->get_runways()->search(ident, int(hdg), &runway); + if(rwyGood) { + activeRwy = runway._rwy_no; + rwy.rwyID = runway._rwy_no; + SG_LOG(SG_ATC, SG_INFO, "In FGGround, active runway for airport " << ident << " is " << activeRwy); + + // Get the threshold position + double other_way = runway._heading - 180.0; + while(other_way <= 0.0) { + other_way += 360.0; + } + // move to the +l end/center of the runway + //cout << "Runway center is at " << runway._lon << ", " << runway._lat << '\n'; + Point3D origin = Point3D(runway._lon, runway._lat, aptElev); + Point3D ref = origin; + double tshlon, tshlat, tshr; + double tolon, tolat, tor; + rwy.length = runway._length * SG_FEET_TO_METER; + geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), other_way, + rwy.length / 2.0 - 25.0, &tshlat, &tshlon, &tshr ); + geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), runway._heading, + rwy.length / 2.0 - 25.0, &tolat, &tolon, &tor ); + // Note - 25 meters in from the runway end is a bit of a hack to put the plane ahead of the user. + // now copy what we need out of runway into rwy + rwy.threshold_pos = Point3D(tshlon, tshlat, aptElev); + Point3D takeoff_end = Point3D(tolon, tolat, aptElev); + //cout << "Threshold position = " << tshlon << ", " << tshlat << ", " << aptElev << '\n'; + //cout << "Takeoff position = " << tolon << ", " << tolat << ", " << aptElev << '\n'; + rwy.hdg = runway._heading; + // Set the projection for the local area based on this active runway + ortho.Init(rwy.threshold_pos, rwy.hdg); + rwy.end1ortho = ortho.ConvertToLocal(rwy.threshold_pos); // should come out as zero + rwy.end2ortho = ortho.ConvertToLocal(takeoff_end); + } else { + SG_LOG(SG_ATC, SG_ALERT, "Help - can't get good runway in FGTower!!"); + activeRwy = "NN"; + } +} + +// Return a random gate ID of an unused gate. +// Two error values may be returned and must be checked for by the calling function: +// -2 signifies that no gates exist at this airport. +// -1 signifies that all gates are currently full. +int FGGround::GetRandomGateID() { + // Check that this airport actually has some gates!! + if(!gates.size()) { + return(-2); + } + + gate_vec_type gateVec; + int num = 0; + int thenum; + int ID; + + gatesItr = gates.begin(); + while(gatesItr != gates.end()) { + if((gatesItr->second)->used == false) { + gateVec.push_back(gatesItr->second); + num++; + } + ++gatesItr; + } + + // Check that there are some unused gates! + if(!gateVec.size()) { + return(-1); + } + + // Randomly select one from the list + sg_srandom_time(); + thenum = (int)(sg_random() * gateVec.size()); + ID = gateVec[thenum]->id; + + return(ID); +} + +// Return a pointer to an unused gate node +Gate* FGGround::GetGateNode() { + int id = GetRandomGateID(); + if(id < 0) { + return(NULL); + } else { + return(gates[id]); + } +} + + +node* FGGround::GetHoldShortNode(const string& rwyID) { + return(NULL); // TODO - either implement me or remove me!!! +} + + +// WARNING - This is hardwired to my prototype logical network format +// and will almost certainly change when Bernie's stuff comes on-line. +// Returns NULL if it can't find a valid node. +node* FGGround::GetThresholdNode(const string& rwyID) { + // For now go through all the nodes and parse their names + // Maybe in the future we'll map threshold nodes by ID + //cout << "Size of network is " << network.size() << '\n'; + for(unsigned int i=0; i<network.size(); ++i) { + //cout << "Name = " << network[i]->name << '\n'; + if(network[i]->name.size()) { + string s = network[i]->name; + // Warning - the next bit is fragile and dependent on my current naming scheme + //cout << "substr = " << s.substr(0,3) << '\n'; + //cout << "size of s = " << s.size() << '\n'; + if(s.substr(0,3) == "rwy") { + //cout << "subsubstr = " << s.substr(4, s.size() - 4) << '\n'; + if(s.substr(4, s.size() - 4) == rwyID) { + return network[i]; + } + } + } + } + return NULL; +} + + +// Get a path from a point on a runway to a gate +// TODO !! + +// Get a path from a node to another node +// Eventually we will need complex algorithms for this taking other traffic, +// shortest path and suitable paths into accout. +// For now we'll just call the shortest path algorithm. +ground_network_path_type FGGround::GetPath(node* A, node* B) { + return(GetShortestPath(A, B)); +}; + +// Get a path from a node to a runway threshold +ground_network_path_type FGGround::GetPath(node* A, const string& rwyID) { + node* b = GetThresholdNode(rwyID); + if(b == NULL) { + SG_LOG(SG_ATC, SG_ALERT, "ERROR - unable to find path to runway theshold in ground.cxx for airport " << ident << '\n'); + ground_network_path_type emptyPath; + emptyPath.erase(emptyPath.begin(), emptyPath.end()); + return(emptyPath); + } + return GetShortestPath(A, b); +} + +// Get a path from a node to a runway hold short point +// Bit of a hack this at the moment! +ground_network_path_type FGGround::GetPathToHoldShort(node* A, const string& rwyID) { + ground_network_path_type path = GetPath(A, rwyID); + path.pop_back(); // That should be the threshold stripped of + path.pop_back(); // and that should be the arc from hold short to threshold + // This isn't robust though - TODO - implement properly! + return(path); +} + +// A shortest path algorithm from memory (ie. I can't find the bl&*dy book again!) +// I'm sure there must be enchancements that we can make to this, such as biasing the +// order in which the nodes are searched out from in favour of those geographically +// closer to the destination. +// Note that we are working with the master set of nodes and arcs so we mustn't change +// or delete them - we only delete the paths that we create during the algorithm. +ground_network_path_type FGGround::GetShortestPath(node* A, node* B) { + a_path* pathPtr; + shortest_path_map_type pathMap; + node_array_type nodesLeft; + + // Debugging check + int pathsCreated = 0; + + // Initialise the algorithm + nodesLeft.push_back(A); + pathPtr = new a_path; + pathsCreated++; + pathPtr->path.push_back(A); + pathPtr->cost = 0; + pathMap[A->nodeID] = pathPtr; + bool solution_found = false; // Flag to indicate that at least one candidate path has been found + int solution_cost = -1; // Cost of current best cost solution. -1 indicates no solution found yet. + a_path solution_path; + + node* nPtr; // nPtr is used to point to the node we are currently working with + + while(nodesLeft.size()) { + //cout << "\n*****nodesLeft*****\n"; + //for(unsigned int i=0; i<nodesLeft.size(); ++i) { + //cout << nodesLeft[i]->nodeID << '\n'; + //} + //cout << "*******************\n\n"; + nPtr = *nodesLeft.begin(); // Thought - definate optimization possibilities here in the choice of which nodes we process first. + nodesLeft.erase(nodesLeft.begin()); + //cout << "Walking out from node " << nPtr->nodeID << '\n'; + for(unsigned int i=0; i<nPtr->arcs.size(); ++i) { + //cout << "ARC TO " << ((nPtr->arcs[i]->n1 == nPtr->nodeID) ? nPtr->arcs[i]->n2 : nPtr->arcs[i]->n1) << '\n'; + } + if((solution_found) && (solution_cost <= pathMap[nPtr->nodeID]->cost)) { + // Do nothing - we've already found a solution and this partial path is already more expensive + } else { + // This path could still be better than the current solution - check it out + for(unsigned int i=0; i<(nPtr->arcs.size()); i++) { + // Map the new path against the end node, ie. *not* the one we just started with. + unsigned int end_nodeID = ((nPtr->arcs[i]->n1 == nPtr->nodeID) ? nPtr->arcs[i]->n2 : nPtr->arcs[i]->n1); + //cout << "end_nodeID = " << end_nodeID << '\n'; + //cout << "pathMap size is " << pathMap.size() << '\n'; + if(end_nodeID == nPtr->nodeID) { + //cout << "Circular arc!\n"; + // Then its a circular arc - don't bother!! + //nPtr->arcs.erase(nPtr->arcs.begin() + i); + } else { + // see if the end node is already in the map or not + if(pathMap.find(end_nodeID) == pathMap.end()) { + //cout << "Not in the map" << endl;; + // Not in the map - easy! + pathPtr = new a_path; + pathsCreated++; + *pathPtr = *pathMap[nPtr->nodeID]; // *copy* the path + pathPtr->path.push_back(nPtr->arcs[i]); + pathPtr->path.push_back(network[end_nodeID]); + pathPtr->cost += nPtr->arcs[i]->distance; + pathMap[end_nodeID] = pathPtr; + nodesLeft.push_back(network[end_nodeID]); // By definition this can't be in the list already, or + // it would also have been in the map and hence OR'd with this one. + if(end_nodeID == B->nodeID) { + //cout << "Solution found!!!" << endl; + // Since this node wasn't in the map this is by definition the first solution + solution_cost = pathPtr->cost; + solution_path = *pathPtr; + solution_found = true; + } + } else { + //cout << "Already in the map" << endl; + // In the map - not so easy - need to get rid of an arc from the higher cost one. + //cout << "Current cost of node " << end_nodeID << " is " << pathMap[end_nodeID]->cost << endl; + int newCost = pathMap[nPtr->nodeID]->cost + nPtr->arcs[i]->distance; + //cout << "New cost is of node " << nPtr->nodeID << " is " << newCost << endl; + if(newCost >= pathMap[end_nodeID]->cost) { + // No need to do anything. + //cout << "Not doing anything!" << endl; + } else { + delete pathMap[end_nodeID]; + pathsCreated--; + + pathPtr = new a_path; + pathsCreated++; + *pathPtr = *pathMap[nPtr->nodeID]; // *copy* the path + pathPtr->path.push_back(nPtr->arcs[i]); + pathPtr->path.push_back(network[end_nodeID]); + pathPtr->cost += nPtr->arcs[i]->distance; + pathMap[end_nodeID] = pathPtr; + + // We need to add this node to the list-to-do again to force a recalculation + // onwards from this node with the new lower cost to node cost. + nodesLeft.push_back(network[end_nodeID]); + + if(end_nodeID == B->nodeID) { + //cout << "Solution found!!!" << endl; + // Need to check if there is a previous better solution + if((solution_cost < 0) || (pathPtr->cost < solution_cost)) { + solution_cost = pathPtr->cost; + solution_path = *pathPtr; + solution_found = true; + } + } + } + } + } + } + } + } + + // delete all the paths before returning + shortest_path_map_iterator spItr = pathMap.begin(); + while(spItr != pathMap.end()) { + if(spItr->second != NULL) { + delete spItr->second; + --pathsCreated; + } + ++spItr; + } + + //cout << "pathsCreated = " << pathsCreated << '\n'; + if(pathsCreated > 0) { + SG_LOG(SG_ATC, SG_ALERT, "WARNING - Possible memory leak in FGGround::GetShortestPath\n\ + Please report to flightgear-devel@flightgear.org\n"); + } + + //cout << (solution_found ? "Result: solution found\n" : "Result: no solution found\n"); + return(solution_path.path); // TODO - we really ought to have a fallback position incase a solution isn't found. +} + + + +// Randomly or otherwise populate some of the gates with parked planes +// (might eventually be done by the AIMgr if and when lots of AI traffic is generated) + +// Return a list of exits from a given runway +// It is up to the calling function to check for non-zero size of returned array before use +node_array_type FGGround::GetExits(const string& rwyID) { + // FIXME - get a 07L or similar in here and we're stuffed!!! + return(runways[atoi(rwyID.c_str())].exits); +} + +void FGGround::RequestDeparture(const PlaneRec& plane, FGAIEntity* requestee) { + // For now we'll just automatically clear all planes to the runway hold. + // This communication needs to be delayed 20 sec or so from receiving the request. + // Even if display=false we still need to start the timer in case display=true when communication starts. + // We also need to bear in mind we also might have other outstanding communications, although for now we'll punt that issue! + // FIXME - sort the above! + + // HACK - assume that anything requesting departure is new for now - FIXME LATER + GroundRec* g = new GroundRec; + g->plane = plane; + g->planePtr = requestee; + g->taxiRequestOutstanding = true; + g->clearanceCounter = 0; + g->cleared = false; + g->incoming = false; + // TODO - need to handle the next 3 as well + //Point3D current_pos; + //node* destination; + //node* last_clearance; + + ground_traffic.push_back(g); +} + +#if 0 +void FGGround::NewArrival(plane_rec plane) { + // What are we going to do here? + // We need to start a new ground_rec and add the plane_rec to it + // We need to decide what gate we are going to clear it to. + // Then we need to add clearing it to that gate to the pending transmissions queue? - or simply transmit? + // Probably simply transmit for now and think about a transmission queue later if we need one. + // We might need one though in order to add a little delay for response time. + ground_rec* g = new ground_rec; + g->plane_rec = plane; + g->current_pos = ConvertWGS84ToXY(plane.pos); + g->node = GetNode(g->current_pos); // TODO - might need to sort out node/arc here + AssignGate(g); + g->cleared = false; + ground_traffic.push_back(g); + NextClearance(g); +} + +void FGGround::NewContact(plane_rec plane) { + // This is a bit of a convienience function at the moment and is likely to change. + if(at a gate or apron) + NewDeparture(plane); + else + NewArrival(plane); +} + +void FGGround::NextClearance(ground_rec &g) { + // Need to work out where we can clear g to. + // Assume the pilot doesn't need progressive instructions + // We *should* already have a gate or holding point assigned by the time we get here + // but it wouldn't do any harm to check. + + // For now though we will hardwire it to clear to the final destination. +} + +void FGGround::AssignGate(ground_rec &g) { + // We'll cheat for now - since we only have the user's aircraft and a couple of airports implemented + // we'll hardwire the gate! + // In the long run the logic of which gate or area to send the plane to could be somewhat non-trivial. +} +#endif //0 + diff --git a/src/ATCDCL/ground.hxx b/src/ATCDCL/ground.hxx new file mode 100644 index 000000000..43f5c0a31 --- /dev/null +++ b/src/ATCDCL/ground.hxx @@ -0,0 +1,367 @@ +// FGGround - a class to provide ground control at larger airports. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_GROUND_HXX +#define _FG_GROUND_HXX + +#include <map> +#include <vector> +#include <list> + +#include <simgear/math/point3d.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/props/props.hxx> + +#include "ATC.hxx" +#include "ATCProjection.hxx" + +#include STL_IOSTREAM +#include STL_STRING + +SG_USING_STD(string); +SG_USING_STD(ios); + +SG_USING_STD(map); +SG_USING_STD(vector); +SG_USING_STD(list); + +class FGAIEntity; +class FGATCMgr; + +////////////////////////////////////////////////////// +// Types for the logical network data structure +enum arc_type { + RUNWAY, + TAXIWAY +}; + +enum node_type { + GATE, + APRON, + HOLD, + JUNCTION, + TJUNCTION +}; + +enum GateType { + TRANSPORT_PASSENGER, + TRANSPORT_PASSENGER_NARROWBODY, + TRANSPORT_PASSENGER_WIDEBODY, + TRANSPORT_CARGO, + GA_LOCAL, + GA_LOCAL_SINGLE, + GA_LOCAL_TWIN, + GA_TRANSIENT, + GA_TRANSIENT_SINGLE, + GA_TRANSIENT_TWIN, + OTHER // ie. anything goes!! +}; + +enum network_element_type { + NODE, + ARC +}; + +struct ground_network_element { + network_element_type struct_type; +}; + +struct arc : public ground_network_element { + int distance; + string name; + arc_type type; + bool directed; //false if 2-way, true if 1-way. + //This is a can of worms since arcs might be one way in different directions under different circumstances + unsigned int n1; // The nodeID of the first node + unsigned int n2; // The nodeID of the second node + // If the arc is directed then flow is normally from n1 to n2. See the above can of worms comment though. +}; + +typedef vector <arc*> arc_array_type; // This was and may become again a list instead of vector +typedef arc_array_type::iterator arc_array_iterator; +typedef arc_array_type::const_iterator arc_array_const_iterator; + +struct node : public ground_network_element { + node(); + ~node(); + + unsigned int nodeID; //each node in an airport needs a unique ID number - this is ZERO-BASED to match array position + Point3D pos; + Point3D orthoPos; + string name; + node_type type; + arc_array_type arcs; + double max_turn_radius; +}; + +typedef vector <node*> node_array_type; +typedef node_array_type::iterator node_array_iterator; +typedef node_array_type::const_iterator node_array_const_iterator; + +struct Gate : public node { + GateType gateType; + int max_weight; //units?? + //airline_code airline; //For the future - we don't have any airline codes ATM + int id; // The gate number in the logical scheme of things + string name; // The real-world gate letter/number + //node* pNode; + bool used; + double heading; // The direction the parked-up plane should point in degrees +}; + +typedef vector < Gate* > gate_vec_type; +typedef gate_vec_type::iterator gate_vec_iterator; +typedef gate_vec_type::const_iterator gate_vec_const_iterator; + +// A map of gate vs. the logical (internal FGFS) gate ID +typedef map < int, Gate* > gate_map_type; +typedef gate_map_type::iterator gate_map_iterator; +typedef gate_map_type::const_iterator gate_map_const_iterator; + +// Runways - all the runway stuff is likely to change in the future +struct Rwy { + int id; //note this is a very simplified scheme for now - R & L are not differentiated + //It should work for simple one rwy airports + node_array_type exits; //Array of available exits from runway + // should probably add an FGRunway structure here as well eventually + // Eventually we will also want some encoding of real-life preferred runways + // This will get us up and running for single runway airports though. +}; + +typedef vector < Rwy > runway_array_type; +typedef runway_array_type::iterator runway_array_iterator; +typedef runway_array_type::const_iterator runway_array_const_iterator; + +// end logical network types +/////////////////////////////////////////////////////// + +/////////////////////////////////////////////////////// +// Structures to use the network + +// A path through the network +typedef vector < ground_network_element* > ground_network_path_type; +typedef ground_network_path_type::iterator ground_network_path_iterator; +typedef ground_network_path_type::const_iterator ground_network_path_const_iterator; + +////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////// +// +// Stuff for the shortest-path algorithms +struct a_path { + a_path(); + + ground_network_path_type path; + int cost; +}; + +// Paths mapped by nodeID reached so-far +typedef map < unsigned int, a_path* > shortest_path_map_type; +typedef shortest_path_map_type::iterator shortest_path_map_iterator; + +// Nodes mapped by their ID +//typedef map < unsigned int, node* > node_map_type; +//typedef node_map_type::iterator node_map_iterator; +//////////////////////////////////////////////// + +// Planes active within the ground network. + +// A more specialist plane rec to include ground information +struct GroundRec { + FGAIEntity* planePtr; // This might move to the planeRec eventually + + PlaneRec plane; + Point3D current_pos; + node* destination; + node* last_clearance; + bool taxiRequestOutstanding; // Plane has requested taxi and we haven't responded yet + double clearanceCounter; // Hack for communication timing - counter since clearance requested in seconds + + bool cleared; // set true when the plane has been cleared to somewhere + bool incoming; //true for arrivals, false for departures + // status? + // Almost certainly need to add more here +}; + +typedef list < GroundRec* > ground_rec_list; +typedef ground_rec_list::iterator ground_rec_list_itr; +typedef ground_rec_list::const_iterator ground_rec_list_const_itr; + +////////////////////////////////////////////////////////////////////////////////////////// + +// Hack +// perhaps we could use an FGRunway instead of this +struct GRunwayDetails { + Point3D threshold_pos; + Point3D end1ortho; // ortho projection end1 (the threshold ATM) + Point3D end2ortho; // ortho projection end2 (the take off end in the current hardwired scheme) + double hdg; // true runway heading + double length; // In *METERS* + string rwyID; +}; + +/////////////////////////////////////////////////////////////////////////////// +// +// FGGround +// +/////////////////////////////////////////////////////////////////////////////// +class FGGround : public FGATC { + +public: + FGGround(); + FGGround(const string& id); + ~FGGround(); + void Init(); + + void Update(double dt); + + inline const string& get_trans_ident() { return trans_ident; } + + // Contact ground control on arrival, assumed to request any gate + //void NewArrival(plane_rec plane); + + // Contact ground control on departure, assumed to request currently active runway. + void RequestDeparture(const PlaneRec& plane, FGAIEntity* requestee); + + // Contact ground control when the calling routine doesn't know if arrival + // or departure is appropriate. + //void NewContact(plane_rec plane); + + // Make a request of ground control. + //void Request(ground_request request); + + // Randomly fill some of the available gates and GA parking spots with planes + void PopulateGates(); + + // Return a suitable gate (maybe this should be a list of suitable gates so the plane or controller can choose the closest one) + void ReturnGate(Gate &gate, GateType type); + + // Return a pointer to an unused gate + Gate* GetGateNode(); + + // Return a pointer to a hold short node + node* GetHoldShortNode(const string& rwyID); + + // Runway stuff - this might change in the future. + // Get a list of exits from a given runway + // It is up to the calling function to check for non-zero size of returned array before use + node_array_type GetExits(const string& rwyID); + + // Get a path from one node to another + ground_network_path_type GetPath(node* A, node* B); + + // Get a path from a node to a runway threshold + ground_network_path_type GetPath(node* A, const string& rwyID); + + // Get a path from a node to a runway hold short point + // Bit of a hack this at the moment! + ground_network_path_type GetPathToHoldShort(node* A, const string& rwyID); + +private: + FGATCMgr* ATCmgr; + // This is purely for synactic convienience to avoid writing globals->get_ATC_mgr()-> all through the code! + + // Need a data structure to hold details of the various active planes + // Need a data structure to hold details of the logical network + // including which gates are filled - or possibly another data structure + // with the positions of the inactive planes. + // Need a data structure to hold outstanding communications from aircraft. + // Possibly need a data structure to hold outstanding communications to aircraft. + + // The logical network + // NODES WILL BE STORED IN THE NETWORK IN ORDER OF nodeID NUMBER + // ie. NODE 5 WILL BE AT network[5] + node_array_type network; + + // A map of all the gates indexed against internal (FGFS) ID + gate_map_type gates; + gate_map_iterator gatesItr; + + FGATCAlignedProjection ortho; + + // Planes currently active + //ground_rec_list ground_traffic; + + // Find the shortest route through the logical network between two points. + //FindShortestRoute(point a, point b); + + // Assign a gate or parking location to a new arrival + //AssignGate(ground_rec &g); + + // Generate the next clearance for an airplane + //NextClearance(ground_rec &g); + + // environment - need to make sure we're getting the surface winds and not winds aloft. + SGPropertyNode_ptr wind_from_hdg; //degrees + SGPropertyNode_ptr wind_speed_knots; //knots + + // for failure modeling + string trans_ident; // transmitted ident + bool ground_failed; // ground failed? + bool networkLoadOK; // Indicates whether LoadNetwork returned true or false at last attempt + + // Tower control + bool untowered; // True if this is an untowered airport (we still need the ground class for shortest path implementation etc + //FGATC* tower; // Pointer to the tower control + + // Logical runway details - this might change in the future. + //runway_array_type runways; // STL way + Rwy runways[37]; // quick hack! + + // Physical runway details + double aptElev; // Airport elevation + string activeRwy; // Active runway number - For now we'll disregard multiple / alternate runway operation. + RunwayDetails rwy; // Assumed to be the active one for now.// Figure out which runways are active. + + // For now we'll just be simple and do one active runway - eventually this will get much more complex + // Copied from FGTower - TODO - it would be better to implement this just once, and have ground call tower + // for runway operation details, but at the moment we can't guarantee that tower control at a given airport + // will be initialised before ground so we can't do that. + void DoRwyDetails(); + + // Load the logical ground network for this airport from file. + // Return true if successfull. + bool LoadNetwork(); + + // Parse a runway exit string and push the supplied node pointer onto the runway exit list + void ParseRwyExits(node* np, char* es); + + // Return a random gate ID of an unused gate. + // Two error values may be returned and must be checked for by the calling function: + // -2 signifies that no gates exist at this airport. + // -1 signifies that all gates are currently full. + // TODO - modify to return a suitable gate based on aircraft size/weight. + int GetRandomGateID(); + + // Return a pointer to the node at a runway threshold + // Returns NULL if unsuccessful. + node* GetThresholdNode(const string& rwyID); + + // A shortest path algorithm from memory (I can't find the bl&*dy book again!) + ground_network_path_type GetShortestPath(node* A, node* B); + + // Planes + ground_rec_list ground_traffic; + ground_rec_list_itr ground_traffic_itr; +}; + +#endif // _FG_GROUND_HXX + diff --git a/src/ATCDCL/tower.cxx b/src/ATCDCL/tower.cxx new file mode 100644 index 000000000..fccaaaba1 --- /dev/null +++ b/src/ATCDCL/tower.cxx @@ -0,0 +1,2671 @@ +// FGTower - a class to provide tower control at towered airports. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// Copyright (C) 2008 Daniyar Atadjanov (ground clearance, gear check, weather, etc.) +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#ifdef HAVE_STRINGS_H +# include <strings.h> // bcopy() +#else +# include <string.h> // MSVC doesn't have strings.h +#endif + +#include <sstream> +#include <iomanip> + +#include <simgear/debug/logstream.hxx> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/math/sg_random.h> +#include <simgear/misc/sg_path.hxx> + +#include <Main/globals.hxx> +#include <Airports/runways.hxx> + +#include "tower.hxx" +#include "ATCmgr.hxx" +#include "ATCutils.hxx" +#include "ATCDialog.hxx" +#include "commlist.hxx" +#include "AILocalTraffic.hxx" + + +SG_USING_STD(cout); + +// TowerPlaneRec + +TowerPlaneRec::TowerPlaneRec() : + planePtr(NULL), + clearedToLand(false), + clearedToLineUp(false), + clearedToTakeOff(false), + holdShortReported(false), + lineUpReported(false), + downwindReported(false), + longFinalReported(false), + longFinalAcknowledged(false), + finalReported(false), + finalAcknowledged(false), + rwyVacatedReported(false), + rwyVacatedAcknowledged(false), + goAroundReported(false), + instructedToGoAround(false), + onRwy(false), + nextOnRwy(false), + vfrArrivalReported(false), + vfrArrivalAcknowledged(false), + opType(TTT_UNKNOWN), + leg(LEG_UNKNOWN), + landingType(AIP_LT_UNKNOWN), + gearWasUp(false), + gearUpReported(false), + isUser(false) +{ + plane.callsign = "UNKNOWN"; +} + +TowerPlaneRec::TowerPlaneRec(const PlaneRec& p) : + planePtr(NULL), + clearedToLand(false), + clearedToLineUp(false), + clearedToTakeOff(false), + holdShortReported(false), + lineUpReported(false), + downwindReported(false), + longFinalReported(false), + longFinalAcknowledged(false), + finalReported(false), + finalAcknowledged(false), + rwyVacatedReported(false), + rwyVacatedAcknowledged(false), + goAroundReported(false), + instructedToGoAround(false), + onRwy(false), + nextOnRwy(false), + vfrArrivalReported(false), + vfrArrivalAcknowledged(false), + opType(TTT_UNKNOWN), + leg(LEG_UNKNOWN), + landingType(AIP_LT_UNKNOWN), + gearWasUp(false), + gearUpReported(false), + isUser(false) +{ + plane = p; +} + +TowerPlaneRec::TowerPlaneRec(const Point3D& pt) : + planePtr(NULL), + clearedToLand(false), + clearedToLineUp(false), + clearedToTakeOff(false), + holdShortReported(false), + lineUpReported(false), + downwindReported(false), + longFinalReported(false), + longFinalAcknowledged(false), + finalReported(false), + finalAcknowledged(false), + rwyVacatedReported(false), + rwyVacatedAcknowledged(false), + goAroundReported(false), + instructedToGoAround(false), + onRwy(false), + nextOnRwy(false), + vfrArrivalReported(false), + vfrArrivalAcknowledged(false), + opType(TTT_UNKNOWN), + leg(LEG_UNKNOWN), + landingType(AIP_LT_UNKNOWN), + gearWasUp(false), + gearUpReported(false), + isUser(false) +{ + plane.callsign = "UNKNOWN"; + pos = pt; +} + +TowerPlaneRec::TowerPlaneRec(const PlaneRec& p, const Point3D& pt) : + planePtr(NULL), + clearedToLand(false), + clearedToLineUp(false), + clearedToTakeOff(false), + holdShortReported(false), + lineUpReported(false), + downwindReported(false), + longFinalReported(false), + longFinalAcknowledged(false), + finalReported(false), + finalAcknowledged(false), + rwyVacatedReported(false), + rwyVacatedAcknowledged(false), + goAroundReported(false), + instructedToGoAround(false), + onRwy(false), + nextOnRwy(false), + vfrArrivalReported(false), + vfrArrivalAcknowledged(false), + opType(TTT_UNKNOWN), + leg(LEG_UNKNOWN), + landingType(AIP_LT_UNKNOWN), + gearWasUp(false), + gearUpReported(false), + isUser(false) +{ + plane = p; + pos = pt; +} + + +// FGTower + +/******************************************* + TODO List + +Currently user is assumed to have taken off again when leaving the runway - check speed/elev for taxiing-in. (MAJOR) + +Use track instead of heading to determine what leg of the circuit the user is flying. (MINOR) + +Use altitude as well as position to try to determine if the user has left the circuit. (MEDIUM - other issues as well). + +Currently HoldShortReported code assumes there will be only one plane holding for the runway at once and +will break when planes start queueing. (CRITICAL) + +Report-Runway-Vacated is left as only user ATC option following a go-around. (MAJOR) + +Report-Downwind is not added as ATC option when user takes off to fly a circuit. (MAJOR) + +eta of USER can be calculated very wrongly in circuit if flying straight out and turn4 etc are with +ve ortho y. +This can then screw up circuit ordering for other planes (MEDIUM) + +USER leaving circuit needs to be more robustly considered when intentions unknown +Currently only considered during climbout and breaks when user turns (MEDIUM). + +GetPos() of the AI planes is called erratically - either too much or not enough. (MINOR) + +GO-AROUND is instructed very late at < 12s to landing - possibly make more dependent on chance of rwy clearing before landing (FEATURE) + +Need to make clear when TowerPlaneRecs do or don't get deleted in RemoveFromCircuitList etc. (MINOR until I misuse it - then CRITICAL!) + +FGTower::RemoveAllUserDialogOptions() really ought to be replaced by an ATCDialog::clear_entries() function. (MINOR - efficiency). + +At the moment planes in the lists are not guaranteed to always have a sensible ETA - it should be set as part of AddList functions, and lists should only be accessed this way. (FAIRLY MAJOR). +*******************************************/ + +FGTower::FGTower() : + separateGround(true), + ground(0) +{ + ATCmgr = globals->get_ATC_mgr(); + + _type = TOWER; + + // Init the property nodes - TODO - need to make sure we're getting surface winds. + wind_from_hdg = fgGetNode("/environment/wind-from-heading-deg", true); + wind_speed_knots = fgGetNode("/environment/wind-speed-kt", true); + + update_count = 0; + update_count_max = 15; + + holdListItr = holdList.begin(); + appListItr = appList.begin(); + depListItr = depList.begin(); + rwyListItr = rwyList.begin(); + circuitListItr = circuitList.begin(); + trafficListItr = trafficList.begin(); + vacatedListItr = vacatedList.begin(); + + freqClear = true; + + timeSinceLastDeparture = 9999; + departed = false; + + nominal_downwind_leg_pos = 1000.0; + nominal_base_leg_pos = -1000.0; + // TODO - set nominal crosswind leg pos based on minimum distance from takeoff end of rwy. + + _departureControlled = false; +} + +FGTower::~FGTower() { + if(!separateGround) { + delete ground; + } +} + +void FGTower::Init() { + //cout << "Initialising tower " << ident << '\n'; + + // Pointers to user's position + user_lon_node = fgGetNode("/position/longitude-deg", true); + user_lat_node = fgGetNode("/position/latitude-deg", true); + user_elev_node = fgGetNode("/position/altitude-ft", true); + user_hdg_node = fgGetNode("/orientation/heading-deg", true); + + // Need some way to initialise rwyOccupied flag correctly if the user is on the runway and to know its the user. + // I'll punt the startup issue for now though!!! + rwyOccupied = false; + + // Setup the ground control at this airport + AirportATC a; + //cout << "Tower ident = " << ident << '\n'; + if(ATCmgr->GetAirportATCDetails(ident, &a)) { + if(a.ground_freq) { // Ground control + ground = (FGGround*)ATCmgr->GetATCPointer(ident, GROUND); + separateGround = true; + if(ground == NULL) { + // Something has gone wrong :-( + SG_LOG(SG_ATC, SG_WARN, "ERROR - ground has frequency but can't get ground pointer :-("); + ground = new FGGround(ident); + separateGround = false; + ground->Init(); + if(_display) { + ground->SetDisplay(); + } else { + ground->SetNoDisplay(); + } + } + } else { + // Initialise ground anyway to do the shortest path stuff! + // Note that we're now responsible for updating and deleting this - NOT the ATCMgr. + ground = new FGGround(ident); + separateGround = false; + ground->Init(); + if(_display) { + ground->SetDisplay(); + } else { + ground->SetNoDisplay(); + } + } + } else { + SG_LOG(SG_ATC, SG_ALERT, "Unable to find airport details for " << ident << " in FGTower::Init()"); + // Initialise ground anyway to avoid segfault later + ground = new FGGround(ident); + separateGround = false; + ground->Init(); + if(_display) { + ground->SetDisplay(); + } else { + ground->SetNoDisplay(); + } + } + + RemoveAllUserDialogOptions(); + + // TODO - attempt to get a departure control pointer to see if we need to hand off departing traffic to departure. + + // Get the airport elevation + aptElev = fgGetAirportElev(ident.c_str()); + + // TODO - this function only assumes one active rwy. + DoRwyDetails(); + + // TODO - this currently assumes only one active runway. + rwyOccupied = OnActiveRunway(Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), 0.0)); + + if(!OnAnyRunway(Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), 0.0), false)) { + //cout << ident << " ADD 0\n"; + current_atcdialog->add_entry(ident, "@AP Tower, @CS @MI miles @CD of the airport for full stop@AT", + "Contact tower for VFR arrival (full stop)", TOWER, + (int)USER_REQUEST_VFR_ARRIVAL_FULL_STOP); + } else { + //cout << "User found on active runway\n"; + // Assume the user is started at the threshold ready to take-off + TowerPlaneRec* t = new TowerPlaneRec; + t->plane.callsign = fgGetString("/sim/user/callsign"); + t->plane.type = GA_SINGLE; // FIXME - hardwired!! + t->opType = TTT_UNKNOWN; // We don't know if the user wants to do circuits or a departure... + t->landingType = AIP_LT_UNKNOWN; + t->leg = TAKEOFF_ROLL; + t->isUser = true; + t->planePtr = NULL; + t->clearedToTakeOff = false; + rwyList.push_back(t); + rwyListItr = rwyList.begin(); + departed = false; + current_atcdialog->add_entry(ident, "@CS @TO", "Request departure / take-off clearance", + TOWER, (int)USER_REQUEST_TAKE_OFF); + } +} + +void FGTower::Update(double dt) { + //cout << "T" << endl; + // Each time step, what do we need to do? + // We need to go through the list of outstanding requests and acknowedgements + // and process at least one of them. + // We need to go through the list of planes under our control and check if + // any need to be addressed. + // We need to check for planes not under our control coming within our + // control area and address if necessary. + + // TODO - a lot of the below probably doesn't need to be called every frame and should be staggered. + + // Sort the arriving planes + + /* + if(ident == "KEMT") { + cout << update_count << "\ttL: " << trafficList.size() << " cL: " << circuitList.size() << " hL: " << holdList.size() << " aL: " << appList.size() << '\n'; + } + */ + //if(ident == "EGNX") cout << display << '\n'; + + if(departed != false) { + timeSinceLastDeparture += dt; + //if(ident == "KEMT") + // cout << " dt = " << dt << " timeSinceLastDeparture = " << timeSinceLastDeparture << '\n'; + } + + //cout << ident << " respond = " << respond << " responseReqd = " << responseReqd << '\n'; + if(respond) { + if(!responseReqd) SG_LOG(SG_ATC, SG_ALERT, "ERROR - respond is true and responseReqd is false in FGTower::Update(...)"); + Respond(); + respond = false; + responseReqd = false; + } + + // Calculate the eta of each plane to the threshold. + // For ground traffic this is the fastest they can get there. + // For air traffic this is the middle approximation. + if(update_count == 1) { + doThresholdETACalc(); + } + + // Order the list of traffic as per expected threshold use and flag any conflicts + if(update_count == 2) { + //bool conflicts = doThresholdUseOrder(); + doThresholdUseOrder(); + } + + // sortConficts() !!! + + if(update_count == 4) { + CheckHoldList(dt); + } + + // Uggh - HACK - why have we got rwyOccupied - wouldn't simply testing rwyList.size() do? + if(rwyList.size()) { + rwyOccupied = true; + } else { + rwyOccupied = false; + } + + if(update_count == 5 && rwyOccupied) { + CheckRunwayList(dt); + } + + if(update_count == 6) { + CheckCircuitList(dt); + } + + if(update_count == 7) { + CheckApproachList(dt); + } + + if(update_count == 8) { + CheckDepartureList(dt); + } + + // TODO - do one plane from the departure list and set departed = false when out of consideration + + //doCommunication(); + + if(!separateGround) { + // The display stuff might have to get more clever than this when not separate + // since the tower and ground might try communicating simultaneously even though + // they're mean't to be the same contoller/frequency!! + // We could also get rid of this by overloading FGATC's Set(No)Display() functions. + if(_display) { + ground->SetDisplay(); + } else { + ground->SetNoDisplay(); + } + ground->Update(dt); + } + + ++update_count; + // How big should ii get - ie how long should the update cycle interval stretch? + if(update_count >= update_count_max) { + update_count = 0; + } + + // Call the base class update for the response time handling. + FGATC::Update(dt); + + /* + if(ident == "KEMT") { + // For AI debugging convienience - may be removed + Point3D user_pos; + user_pos.setlon(user_lon_node->getDoubleValue()); + user_pos.setlat(user_lat_node->getDoubleValue()); + user_pos.setelev(user_elev_node->getDoubleValue()); + Point3D user_ortho_pos = ortho.ConvertToLocal(user_pos); + fgSetDouble("/AI/user/ortho-x", user_ortho_pos.x()); + fgSetDouble("/AI/user/ortho-y", user_ortho_pos.y()); + fgSetDouble("/AI/user/elev", user_elev_node->getDoubleValue()); + } + */ + + //cout << "Done T" << endl; +} + +void FGTower::ReceiveUserCallback(int code) { + if(code == (int)USER_REQUEST_VFR_DEPARTURE) { + RequestDepartureClearance("USER"); + } else if(code == (int)USER_REQUEST_VFR_ARRIVAL) { + VFRArrivalContact("USER"); + } else if(code == (int)USER_REQUEST_VFR_ARRIVAL_FULL_STOP) { + VFRArrivalContact("USER", FULL_STOP); + } else if(code == (int)USER_REQUEST_VFR_ARRIVAL_TOUCH_AND_GO) { + VFRArrivalContact("USER", TOUCH_AND_GO); + } else if(code == (int)USER_REPORT_DOWNWIND) { + ReportDownwind("USER"); + } else if(code == (int)USER_REPORT_3_MILE_FINAL) { + // For now we'll just call report final instead of report long final to avoid having to alter the response code + ReportFinal("USER"); + } else if(code == (int)USER_REPORT_RWY_VACATED) { + ReportRunwayVacated("USER"); + } else if(code == (int)USER_REPORT_GOING_AROUND) { + ReportGoingAround("USER"); + } else if(code == (int)USER_REQUEST_TAKE_OFF) { + RequestTakeOffClearance("USER"); + } +} + +// **************** RESPONSE FUNCTIONS **************** + +void FGTower::Respond() { + //cout << "\nEntering Respond, responseID = " << responseID << endl; + TowerPlaneRec* t = FindPlane(responseID); + if(t) { + // This will grow!!! + if(t->vfrArrivalReported && !t->vfrArrivalAcknowledged) { + //cout << "Tower " << ident << " is responding to VFR arrival reported...\n"; + // Testing - hardwire straight in for now + string trns = t->plane.callsign; + trns += " "; + trns += name; + trns += " Tower,"; + // Should we clear staight in or for downwind entry? + // For now we'll clear straight in if greater than 1km from a line drawn through the threshold perpendicular to the rwy. + // Later on we might check the actual heading and direct some of those to enter on downwind or base. + Point3D op = ortho.ConvertToLocal(t->pos); + float gp = fgGetFloat("/gear/gear/position-norm"); + if(gp < 1) + t->gearWasUp = true; // This will be needed on final to tell "Gear down, ready to land." + if(op.y() < -1000) { + trns += " Report three mile straight-in runway "; + t->opType = STRAIGHT_IN; + if(t->isUser) { + current_atcdialog->add_entry(ident, "@CS @MI mile final runway @RW@GR", "Report Final", TOWER, (int)USER_REPORT_3_MILE_FINAL); + } else { + t->planePtr->RegisterTransmission(14); + } + } else { + // For now we'll just request reporting downwind. + // TODO - In real life something like 'report 2 miles southwest right downwind rwy 19R' might be used + // but I'm not sure how to handle all permutations of which direction to tell to report from yet. + trns += " Report "; + //cout << "Responding, rwy.patterDirection is " << rwy.patternDirection << '\n'; + trns += ((rwy.patternDirection == 1) ? "right " : "left "); + trns += "downwind runway "; + t->opType = CIRCUIT; + // leave it in the app list until it gets into pattern though. + if(t->isUser) { + current_atcdialog->add_entry(ident, "@AP Tower, @CS Downwind @RW", "Report Downwind", TOWER, (int)USER_REPORT_DOWNWIND); + } else { + t->planePtr->RegisterTransmission(15); + } + } + trns += ConvertRwyNumToSpokenString(activeRwy); + if(_display) { + pending_transmission = trns; + Transmit(); + } else { + //cout << "Not displaying, trns was " << trns << '\n'; + } + t->vfrArrivalAcknowledged = true; + } else if(t->downwindReported) { + //cout << "Tower " << ident << " is responding to downwind reported...\n"; + ProcessDownwindReport(t); + t->downwindReported = false; + } else if(t->lineUpReported) { + string trns = t->plane.callsign; + if(rwyOccupied) { + double f = globals->get_ATC_mgr()->GetFrequency(ident, ATIS) / 100.0; + string wtr; + if(!f) { + wtr = ", " + GetWeather(); + } + trns += " Cleared for take-off" + wtr; + t->clearedToTakeOff = true; + } else { + if(!OnAnyRunway(Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), 0.0), true)) { + // TODO: Check if any AI Planes on final and tell something like: "After the landing CALLSIGN line up runway two eight right" + trns += " Line up runway " + ConvertRwyNumToSpokenString(activeRwy); + t->clearedToTakeOff = false; + current_atcdialog->add_entry(ident, "@CS @TO", "Report ready for take-off", TOWER, (int)USER_REQUEST_TAKE_OFF); + + } else { + sg_srandom_time(); + if((int(sg_random() * 10) + 1) != 3) { + t->clearedToTakeOff = true; + trns += " Cleared immediate take-off "; + } else { + t->clearedToTakeOff = false; + trns += " Negative, departure runway " + ConvertRwyNumToSpokenString(activeRwy); + } + } + } + if(_display) { + pending_transmission = trns; + Transmit(); + } else { + //cout << "Not displaying, trns was " << trns << '\n'; + } + t->lineUpReported = false; + } else if(t->holdShortReported) { + //cout << "Tower " << ident << " is reponding to holdShortReported...\n"; + if(t->nextOnRwy) { + if(rwyOccupied) { // TODO - ought to add a sanity check that it isn't this plane only on the runway (even though it shouldn't be!!) + // Do nothing for now - consider acknowloging hold short eventually + } else { + ClearHoldingPlane(t); + t->leg = TAKEOFF_ROLL; + rwyList.push_back(t); + rwyListItr = rwyList.begin(); + rwyOccupied = true; + // WARNING - WE ARE ASSUMING ONLY ONE PLANE REPORTING HOLD AT A TIME BELOW + // FIXME TODO - FIX THIS!!! + if(!holdList.empty()) { + if(holdListItr == holdList.end()) { + holdListItr = holdList.begin(); + } + holdList.erase(holdListItr); + holdListItr = holdList.begin(); + } + } + } else { + // Tell him to hold and what position he is. + // Not currently sure under which circumstances we do or don't bother transmitting this. + string trns = t->plane.callsign; + trns += " hold position"; + if(_display) { + pending_transmission = trns; + Transmit(); + } + // TODO - add some idea of what traffic is blocking him. + } + t->holdShortReported = false; + } else if(t->finalReported && !(t->finalAcknowledged)) { + //cout << "Tower " << ident << " is responding to finalReported...\n"; + bool disp = true; + string trns = t->plane.callsign; + //cout << (t->nextOnRwy ? "Next on rwy " : "Not next!! "); + //cout << (rwyOccupied ? "RWY OCCUPIED!!\n" : "Rwy not occupied\n"); + if(t->nextOnRwy && !rwyOccupied && !(t->instructedToGoAround)) { + if(t->landingType == FULL_STOP) { + trns += " cleared to land "; + } else { + double f = globals->get_ATC_mgr()->GetFrequency(ident, ATIS) / 100.0; + string wtr; + if(!f) { + wtr = ", " + GetWeather(); + } else { + wtr = ", runway " + ConvertRwyNumToSpokenString(activeRwy); + } + trns += " cleared to land" + wtr; + } + // TODO - add winds + t->clearedToLand = true; + // Maybe remove report downwind from menu here as well incase user didn't bother to? + if(t->isUser) { + //cout << "ADD VACATED B\n"; + // Put going around at the top (and hence default) since that'll be more desperate, + // or put rwy vacated at the top since that'll be more common? + current_atcdialog->add_entry(ident, "@CS Going Around", "Report going around", TOWER, USER_REPORT_GOING_AROUND); + current_atcdialog->add_entry(ident, "@CS Clear of the runway", "Report runway vacated", TOWER, USER_REPORT_RWY_VACATED); + } else { + t->planePtr->RegisterTransmission(7); + } + } else if(t->eta < 20) { + // Do nothing - we'll be telling it to go around in less than 10 seconds if the + // runway doesn't clear so no point in calling "continue approach". + disp = false; + } else { + trns += " continue approach"; + trns += " and report "; + trns += ((rwy.patternDirection == 1) ? "right " : "left "); + trns += "downwind runway " + ConvertRwyNumToSpokenString(activeRwy); + t->opType = CIRCUIT; + if(t->isUser) { + current_atcdialog->add_entry(ident, "@AP Tower, @CS Downwind @RW", "Report Downwind", TOWER, (int)USER_REPORT_DOWNWIND); + } else { + t->planePtr->RegisterTransmission(15); + } + t->clearedToLand = false; + } + if(_display && disp) { + pending_transmission = trns; + Transmit(); + } + t->finalAcknowledged = true; + } else if(t->rwyVacatedReported && !(t->rwyVacatedAcknowledged)) { + ProcessRunwayVacatedReport(t); + t->rwyVacatedAcknowledged = true; + } + } + //freqClear = true; // FIXME - set this to come true after enough time to render the message + _releaseCounter = 0.0; + _releaseTime = 5.5; + _runReleaseCounter = true; + //cout << "Done Respond\n" << endl; +} + +void FGTower::ProcessDownwindReport(TowerPlaneRec* t) { + int i = 1; + int a = 0; // Count of preceding planes on approach + bool cf = false; // conflicting traffic on final + bool cc = false; // preceding traffic in circuit + TowerPlaneRec* tc = NULL; + for(tower_plane_rec_list_iterator twrItr = circuitList.begin(); twrItr != circuitList.end(); twrItr++) { + if((*twrItr)->plane.callsign == responseID) break; + tc = *twrItr; + ++i; + } + if(i > 1) { cc = true; } + doThresholdETACalc(); + TowerPlaneRec* tf = NULL; + for(tower_plane_rec_list_iterator twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + if((*twrItr)->eta < (t->eta + 45) && strcmp((*twrItr)->plane.callsign.c_str(), t->plane.callsign.c_str()) != 0) { // don't let ATC ask you to follow yourself + a++; + tf = *twrItr; + cf = true; + // This should set the flagged plane to be the last conflicting one, and hence the one to follow. + // It ignores the fact that we might have problems slotting into the approach traffic behind it - + // eventually we'll need some fancy algorithms for that! + } + } + string trns = t->plane.callsign; + trns += " Number "; + trns += ConvertNumToSpokenDigits(i + a); + // This assumes that the number spoken is landing position, not circuit position, since some of the traffic might be on straight-in final. + trns += " "; + TowerPlaneRec* tt = NULL; + if((i == 1) && rwyList.empty() && (t->nextOnRwy) && (!cf)) { // Unfortunately nextOnRwy currently doesn't handle circuit/straight-in ordering properly at present, hence the cf check below. + trns += "Cleared to land"; // TODO - clear for the option if appropriate + t->clearedToLand = true; + if(!t->isUser) t->planePtr->RegisterTransmission(7); + } else if((i+a) > 1) { + //First set tt to point to the correct preceding plane - final or circuit + if(tc && tf) { + tt = (tf->eta < tc->eta ? tf : tc); + } else if(tc) { + tt = tc; + } else if(tf) { + tt = tf; + } else { + // We should never get here! + SG_LOG(SG_ATC, SG_ALERT, "ALERT - Logic error in FGTower::ProcessDownwindReport"); + return; + } + trns += "Follow the "; + string s = tt->plane.callsign; + int p = s.find('-'); + s = s.substr(0,p); + trns += s; + if((tt->opType) == CIRCUIT) { + PatternLeg leg; + if(tt->isUser) { + leg = tt->leg; + } else { + leg = tt->planePtr->GetLeg(); + } + if(leg == FINAL) { + trns += " on final"; + } else if(leg == TURN4) { + trns += " turning final"; + } else if(leg == BASE) { + trns += " on base"; + } else if(leg == TURN3) { + trns += " turning base"; + } + } else { + double miles_out = CalcDistOutMiles(tt); + if(miles_out < 2) { + trns += " on short final"; + } else { + trns += " on "; + trns += ConvertNumToSpokenDigits((int)miles_out); + trns += " mile final"; + } + } + } + if(_display) { + pending_transmission = trns; + Transmit(); + } + if(t->isUser) { + if(t->opType == TTT_UNKNOWN) t->opType = CIRCUIT; + //cout << "ADD VACATED A\n"; + // Put going around at the top (and hence default) since that'll be more desperate, + // or put rwy vacated at the top since that'll be more common? + //cout << "ident = " << ident << ", adding go-around option\n"; + current_atcdialog->add_entry(ident, "@CS Going Around", "Report going around", TOWER, USER_REPORT_GOING_AROUND); + current_atcdialog->add_entry(ident, "@CS Clear of the runway", "Report runway vacated", TOWER, USER_REPORT_RWY_VACATED); + } +} + +void FGTower::ProcessRunwayVacatedReport(TowerPlaneRec* t) { + //cout << "Processing rwy vacated...\n"; + if(t->isUser) current_atcdialog->remove_entry(ident, USER_REPORT_GOING_AROUND, TOWER); + string trns = t->plane.callsign; + if(separateGround) { + trns += " Contact ground on "; + double f = globals->get_ATC_mgr()->GetFrequency(ident, GROUND) / 100.0; + char buf[10]; + sprintf(buf, "%.2f", f); + trns += buf; + trns += " Good Day"; + if(!t->isUser) t->planePtr->RegisterTransmission(5); + } else { + // Cop-out!! + trns += " cleared for taxi to general aviation parking"; + if(!t->isUser) t->planePtr->RegisterTransmission(6); // TODO - this is a mega-hack!! + } + //cout << "trns = " << trns << '\n'; + if(_display) { + pending_transmission = trns; + Transmit(); + } + RemoveFromRwyList(t->plane.callsign); + AddToVacatedList(t); + // Maybe we should check that the plane really *has* vacated the runway! +} + +// *********** END RESPONSE FUNCTIONS ***************** + +// Currently this assumes we *are* next on the runway and doesn't check for planes about to land - +// this should be done prior to calling this function. +void FGTower::ClearHoldingPlane(TowerPlaneRec* t) { + //cout << "Entering ClearHoldingPlane..." << endl; + // Lets Roll !!!! + string trns = t->plane.callsign; + //if(departed plane < some threshold in time away) { + if(0) { // FIXME + //if(timeSinceLastDeparture <= 60.0 && departed == true) { + trns += " line up runway " + ConvertRwyNumToSpokenString(activeRwy); + t->clearedToLineUp = true; + t->planePtr->RegisterTransmission(3); // cleared to line-up + //} else if(arriving plane < some threshold away) { + } else if(GetTrafficETA(2) < 150.0 && (timeSinceLastDeparture > 60.0 || departed == false)) { // Hack - hardwired time + trns += " cleared immediate take-off"; + if(trafficList.size()) { + tower_plane_rec_list_iterator trfcItr = trafficList.begin(); + trfcItr++; // At the moment the holding plane should be first in trafficList. + // Note though that this will break if holding planes aren't put in trafficList in the future. + TowerPlaneRec* trfc = *trfcItr; + trns += "... traffic is"; + switch(trfc->plane.type) { + case UNKNOWN: + break; + case GA_SINGLE: + trns += " a Cessna"; // TODO - add ability to specify actual plane type somewhere + break; + case GA_HP_SINGLE: + trns += " a Piper"; + break; + case GA_TWIN: + trns += " a King-air"; + break; + case GA_JET: + trns += " a Learjet"; + break; + case MEDIUM: + trns += " a Regional"; + break; + case HEAVY: + trns += " a Heavy"; + break; + case MIL_JET: + trns += " Military"; + break; + } + //if(trfc->opType == STRAIGHT_IN || trfc->opType == TTT_UNKNOWN) { + if(trfc->opType == STRAIGHT_IN) { + double miles_out = CalcDistOutMiles(trfc); + if(miles_out < 2) { + trns += " on final"; + } else { + trns += " on "; + trns += ConvertNumToSpokenDigits((int)miles_out); + trns += " mile final"; + } + } else if(trfc->opType == CIRCUIT) { + //cout << "Getting leg of " << trfc->plane.callsign << '\n'; + switch(trfc->leg) { + case FINAL: + trns += " on final"; + break; + case TURN4: + trns += " turning final"; + break; + case BASE: + trns += " on base"; + break; + case TURN3: + trns += " turning base"; + break; + case DOWNWIND: + trns += " in circuit"; // At the moment the user plane is generally flagged as unknown opType when downwind incase its a downwind departure which means we won't get here. + break; + // And to eliminate compiler warnings... + case TAKEOFF_ROLL: break; + case CLIMBOUT: break; + case TURN1: break; + case CROSSWIND: break; + case TURN2: break; + case LANDING_ROLL: break; + case LEG_UNKNOWN: break; + } + } + } else { + // By definition there should be some arriving traffic if we're cleared for immediate takeoff + SG_LOG(SG_ATC, SG_WARN, "Warning: Departing traffic cleared for *immediate* take-off despite no arriving traffic in FGTower"); + } + t->clearedToTakeOff = true; + t->planePtr->RegisterTransmission(4); // cleared to take-off - TODO differentiate between immediate and normal take-off + departed = false; + timeSinceLastDeparture = 0.0; + } else { + //} else if(timeSinceLastDeparture > 60.0 || departed == false) { // Hack - test for timeSinceLastDeparture should be in lineup block eventually + trns += " cleared for take-off"; + // TODO - add traffic is... ? + t->clearedToTakeOff = true; + t->planePtr->RegisterTransmission(4); // cleared to take-off + departed = false; + timeSinceLastDeparture = 0.0; + } + if(_display) { + pending_transmission = trns; + Transmit(); + } + //cout << "Done ClearHoldingPlane " << endl; +} + + +// *************************************************************************************** +// ********** Functions to periodically check what the various traffic is doing ********** + +// Do one plane from the hold list +void FGTower::CheckHoldList(double dt) { + //cout << "Entering CheckHoldList..." << endl; + if(!holdList.empty()) { + //cout << "*holdListItr = " << *holdListItr << endl; + if(holdListItr == holdList.end()) { + holdListItr = holdList.begin(); + } + //cout << "*holdListItr = " << *holdListItr << endl; + //Process(*holdListItr); + TowerPlaneRec* t = *holdListItr; + //cout << "t = " << t << endl; + if(t->holdShortReported) { + // NO-OP - leave it to the response handler. + } else { // not responding to report, but still need to clear if clear + if(t->nextOnRwy) { + //cout << "departed = " << departed << '\n'; + //cout << "timeSinceLastDeparture = " << timeSinceLastDeparture << '\n'; + if(rwyOccupied) { + RemoveAllUserDialogOptions(); + current_atcdialog->add_entry(ident, "@CS Ready for take-off", "Request take-off clearance", TOWER, (int)USER_REQUEST_TAKE_OFF); + } else if(timeSinceLastDeparture <= 60.0 && departed == true) { + // Do nothing - this is a bit of a hack - should maybe do line up be ready here + } else { + ClearHoldingPlane(t); + t->leg = TAKEOFF_ROLL; + rwyList.push_back(t); + rwyListItr = rwyList.begin(); + rwyOccupied = true; + holdList.erase(holdListItr); + holdListItr = holdList.begin(); + if (holdList.empty()) + return; + } + } + // TODO - rationalise the considerable code duplication above! + } + ++holdListItr; + } + //cout << "Done CheckHoldList" << endl; +} + +// do the ciruit list +void FGTower::CheckCircuitList(double dt) { + //cout << "Entering CheckCircuitList..." << endl; + // Clear the constraints - we recalculate here. + base_leg_pos = 0.0; + downwind_leg_pos = 0.0; + crosswind_leg_pos = 0.0; + + if(!circuitList.empty()) { // Do one plane from the circuit + if(circuitListItr == circuitList.end()) { + circuitListItr = circuitList.begin(); + } + TowerPlaneRec* t = *circuitListItr; + //cout << ident << ' ' << circuitList.size() << ' ' << t->plane.callsign << " " << t->leg << " eta " << t->eta << '\n'; + if(t->isUser) { + t->pos.setlon(user_lon_node->getDoubleValue()); + t->pos.setlat(user_lat_node->getDoubleValue()); + t->pos.setelev(user_elev_node->getDoubleValue()); + //cout << ident << ' ' << circuitList.size() << ' ' << t->plane.callsign << " " << t->leg << " eta " << t->eta << '\n'; + } else { + t->pos = t->planePtr->GetPos(); // We should probably only set the pos's on one walk through the traffic list in the update function, to save a few CPU should we end up duplicating this. + t->landingType = t->planePtr->GetLandingOption(); + //cout << "AI plane landing option is " << t->landingType << '\n'; + } + Point3D tortho = ortho.ConvertToLocal(t->pos); + if(t->isUser) { + // Need to figure out which leg he's on + //cout << "rwy.hdg = " << rwy.hdg << " user hdg = " << user_hdg_node->getDoubleValue(); + double ho = GetAngleDiff_deg(user_hdg_node->getDoubleValue(), rwy.hdg); + //cout << " ho = " << ho << " abs(ho = " << abs(ho) << '\n'; + // TODO FIXME - get the wind and convert this to track, or otherwise use track somehow!!! + // If it's gusty might need to filter the value, although we are leaving 30 degrees each way leeway! + if(fabs(ho) < 30) { + // could be either takeoff, climbout or landing - check orthopos.y + //cout << "tortho.y = " << tortho.y() << '\n'; + if((tortho.y() < 0) || (t->leg == TURN4) || (t->leg == FINAL)) { + t->leg = FINAL; + //cout << "Final\n"; + } else { + t->leg = CLIMBOUT; // TODO - check elev wrt. apt elev to differentiate takeoff roll and climbout + //cout << "Climbout\n"; + // If it's the user we may be unsure of his/her intentions. + // (Hopefully the AI planes won't try confusing the sim!!!) + //cout << "tortho.y = " << tortho.y() << '\n'; + if(t->opType == TTT_UNKNOWN) { + if(tortho.y() > 5000) { + // 5 km out from threshold - assume it's a departure + t->opType = OUTBOUND; // TODO - could check if the user has climbed significantly above circuit altitude as well. + // Since we are unknown operation we should be in depList already. + //cout << ident << " Removing user from circuitList (TTT_UNKNOWN)\n"; + circuitListItr = circuitList.erase(circuitListItr); + RemoveFromTrafficList(t->plane.callsign); + if (circuitList.empty()) + return; + } + } else if(t->opType == CIRCUIT) { + if(tortho.y() > 10000) { + // 10 km out - assume the user has abandoned the circuit!! + t->opType = OUTBOUND; + depList.push_back(t); + depListItr = depList.begin(); + //cout << ident << " removing user from circuitList (CIRCUIT)\n"; + circuitListItr = circuitList.erase(circuitListItr); + if (circuitList.empty()) + return; + } + } + } + } else if(fabs(ho) < 60) { + // turn1 or turn 4 + // TODO - either fix or doublecheck this hack by looking at heading and pattern direction + if((t->leg == CLIMBOUT) || (t->leg == TURN1)) { + t->leg = TURN1; + //cout << "Turn1\n"; + } else { + t->leg = TURN4; + //cout << "Turn4\n"; + } + } else if(fabs(ho) < 120) { + // crosswind or base + // TODO - either fix or doublecheck this hack by looking at heading and pattern direction + if((t->leg == TURN1) || (t->leg == CROSSWIND)) { + t->leg = CROSSWIND; + //cout << "Crosswind\n"; + } else { + t->leg = BASE; + //cout << "Base\n"; + } + } else if(fabs(ho) < 150) { + // turn2 or turn 3 + // TODO - either fix or doublecheck this hack by looking at heading and pattern direction + if((t->leg == CROSSWIND) || (t->leg == TURN2)) { + t->leg = TURN2; + //cout << "Turn2\n"; + } else { + t->leg = TURN3; + // Probably safe now to assume the user is flying a circuit + t->opType = CIRCUIT; + //cout << "Turn3\n"; + } + } else { + // downwind + t->leg = DOWNWIND; + //cout << "Downwind\n"; + } + if(t->leg == FINAL) { + if(OnActiveRunway(t->pos)) { + t->leg = LANDING_ROLL; + } + } + } else { + t->leg = t->planePtr->GetLeg(); + } + + // Set the constraints IF this is the first plane in the circuit + // TODO - at the moment we're constraining plane 2 based on plane 1 - this won't (or might not) work for 3 planes in the circuit!! + if(circuitListItr == circuitList.begin()) { + switch(t->leg) { + case FINAL: + // Base leg must be at least as far out as the plane is - actually possibly not necessary for separation, but we'll use that for now. + base_leg_pos = tortho.y(); + //cout << "base_leg_pos = " << base_leg_pos << '\n'; + break; + case TURN4: + // Fall through to base + case BASE: + base_leg_pos = tortho.y(); + //cout << "base_leg_pos = " << base_leg_pos << '\n'; + break; + case TURN3: + // Fall through to downwind + case DOWNWIND: + // Only have the downwind leg pos as turn-to-base constraint if more negative than we already have. + base_leg_pos = (tortho.y() < base_leg_pos ? tortho.y() : base_leg_pos); + //cout << "base_leg_pos = " << base_leg_pos; + downwind_leg_pos = tortho.x(); // Assume that a following plane can simply be constrained by the immediately in front downwind plane + //cout << " downwind_leg_pos = " << downwind_leg_pos << '\n'; + break; + case TURN2: + // Fall through to crosswind + case CROSSWIND: + crosswind_leg_pos = tortho.y(); + //cout << "crosswind_leg_pos = " << crosswind_leg_pos << '\n'; + t->instructedToGoAround = false; + break; + case TURN1: + // Fall through to climbout + case CLIMBOUT: + // Only use current by constraint as largest + crosswind_leg_pos = (tortho.y() > crosswind_leg_pos ? tortho.y() : crosswind_leg_pos); + //cout << "crosswind_leg_pos = " << crosswind_leg_pos << '\n'; + break; + case TAKEOFF_ROLL: + break; + case LEG_UNKNOWN: + break; + case LANDING_ROLL: + break; + default: + break; + } + } + + if(t->leg == FINAL && !(t->instructedToGoAround)) { + doThresholdETACalc(); + doThresholdUseOrder(); + /* + if(t->isUser) { + cout << "Checking USER on final... "; + cout << "eta " << t->eta; + if(t->clearedToLand) cout << " cleared to land\n"; + } + */ + //cout << "YES FINAL, t->eta = " << t->eta << ", rwyList.size() = " << rwyList.size() << '\n'; + if(t->landingType == FULL_STOP) { + t->opType = INBOUND; + //cout << "\n******** SWITCHING TO INBOUND AT POINT AAA *********\n\n"; + } + if(t->eta < 12 && rwyList.size()) { + // TODO - need to make this more sophisticated + // eg. is the plane accelerating down the runway taking off [OK], + // or stationary near the start [V. BAD!!]. + // For now this should stop the AI plane landing on top of the user. + tower_plane_rec_list_iterator twrItr; + twrItr = rwyList.begin(); + TowerPlaneRec* tpr = *twrItr; + if(strcmp(tpr->plane.callsign.c_str(), t->plane.callsign.c_str()) == 0 + && rwyList.size() == 1) { + // Fixing bug when ATC says that we must go around because of traffic on rwy + // but that traffic is our plane! In future we can use this expression + // for other ATC-messages like "On ground at 46, vacate left." + + } else { + string trns = t->plane.callsign; + trns += " GO AROUND TRAFFIC ON RUNWAY I REPEAT GO AROUND"; + pending_transmission = trns; + ImmediateTransmit(); + t->instructedToGoAround = true; + t->clearedToLand = false; + // Assume it complies!!! + t->opType = CIRCUIT; + t->leg = CLIMBOUT; + if(t->planePtr) { + //cout << "Registering Go-around transmission with AI plane\n"; + t->planePtr->RegisterTransmission(13); + } + } + } else if(!t->clearedToLand) { + // The whip through the appList is a hack since currently t->nextOnRwy doesn't always work + // TODO - fix this! + bool cf = false; + for(tower_plane_rec_list_iterator twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + if((*twrItr)->eta < t->eta) { + cf = true; + } + } + if(t->nextOnRwy && !cf) { + if(!rwyList.size()) { + string trns = t->plane.callsign; + trns += " Cleared to land"; + pending_transmission = trns; + Transmit(); + //if(t->isUser) cout << "Transmitting cleared to Land!!!\n"; + t->clearedToLand = true; + if(!t->isUser) { + t->planePtr->RegisterTransmission(7); + } + } + } else { + //if(t->isUser) cout << "Not next\n"; + } + } + } else if(t->leg == LANDING_ROLL) { + //cout << t->plane.callsign << " has landed - adding to rwyList\n"; + rwyList.push_front(t); + // TODO - if(!clearedToLand) shout something!! + t->clearedToLand = false; + RemoveFromTrafficList(t->plane.callsign); + if(t->isUser) { + t->opType = TTT_UNKNOWN; + } // TODO - allow the user to specify opType via ATC menu + //cout << ident << " Removing " << t->plane.callsign << " from circuitList..." << endl; + circuitListItr = circuitList.erase(circuitListItr); + if(circuitListItr == circuitList.end() ) { + circuitListItr = circuitList.begin(); + // avoid increment of circuitListItr (would increment to second element, or crash if no element left) + return; + } + } + ++circuitListItr; + } + //cout << "Done CheckCircuitList" << endl; +} + +// Do the runway list - we'll do the whole runway list since it's important and there'll never be many planes on the rwy at once!! +// FIXME - at the moment it looks like we're only doing the first plane from the rwy list. +// (However, at the moment there should only be one airplane on the rwy at once, until we +// start allowing planes to line up whilst previous arrival clears the rwy.) +void FGTower::CheckRunwayList(double dt) { + //cout << "Entering CheckRunwayList..." << endl; + if(rwyOccupied) { + if(!rwyList.size()) { + rwyOccupied = false; + } else { + rwyListItr = rwyList.begin(); + TowerPlaneRec* t = *rwyListItr; + if(t->isUser) { + t->pos.setlon(user_lon_node->getDoubleValue()); + t->pos.setlat(user_lat_node->getDoubleValue()); + t->pos.setelev(user_elev_node->getDoubleValue()); + } else { + t->pos = t->planePtr->GetPos(); // We should probably only set the pos's on one walk through the traffic list in the update function, to save a few CPU should we end up duplicating this. + } + bool on_rwy = OnActiveRunway(t->pos); + if(!on_rwy) { + // TODO - for all of these we need to check what the user is *actually* doing! + if((t->opType == INBOUND) || (t->opType == STRAIGHT_IN)) { + //cout << "Tower " << ident << " is removing plane " << t->plane.callsign << " from rwy list (vacated)\n"; + //cout << "Size of rwylist was " << rwyList.size() << '\n'; + //cout << "Size of vacatedList was " << vacatedList.size() << '\n'; + RemoveFromRwyList(t->plane.callsign); + AddToVacatedList(t); + //cout << "Size of rwylist is " << rwyList.size() << '\n'; + //cout << "Size of vacatedList is " << vacatedList.size() << '\n'; + // At the moment we wait until Runway Vacated is reported by the plane before telling to contact ground etc. + // It's possible we could be a bit more proactive about this. + } else if(t->opType == OUTBOUND) { + depList.push_back(t); + depListItr = depList.begin(); + rwyList.pop_front(); + departed = true; + timeSinceLastDeparture = 0.0; + } else if(t->opType == CIRCUIT) { + //cout << ident << " adding " << t->plane.callsign << " to circuitList" << endl; + circuitList.push_back(t); + circuitListItr = circuitList.begin(); + AddToTrafficList(t); + rwyList.pop_front(); + departed = true; + timeSinceLastDeparture = 0.0; + } else if(t->opType == TTT_UNKNOWN) { + depList.push_back(t); + depListItr = depList.begin(); + //cout << ident << " adding " << t->plane.callsign << " to circuitList" << endl; + circuitList.push_back(t); + circuitListItr = circuitList.begin(); + AddToTrafficList(t); + rwyList.pop_front(); + departed = true; + timeSinceLastDeparture = 0.0; // TODO - we need to take into account that the user might taxi-in when flagged opType UNKNOWN - check speed/altitude etc to make decision as to what user is up to. + } else { + // HELP - we shouldn't ever get here!!! + } + } + } + } + //cout << "Done CheckRunwayList" << endl; +} + +// Do one plane from the approach list +void FGTower::CheckApproachList(double dt) { + //cout << "CheckApproachList called for " << ident << endl; + //cout << "AppList.size is " << appList.size() << endl; + if(!appList.empty()) { + if(appListItr == appList.end()) { + appListItr = appList.begin(); + } + TowerPlaneRec* t = *appListItr; + //cout << "t = " << t << endl; + //cout << "Checking " << t->plane.callsign << endl; + if(t->isUser) { + t->pos.setlon(user_lon_node->getDoubleValue()); + t->pos.setlat(user_lat_node->getDoubleValue()); + t->pos.setelev(user_elev_node->getDoubleValue()); + } else { + // TODO - set/update the position if it's an AI plane + } + doThresholdETACalc(); // We need this here because planes in the lists are not guaranteed to *always* have the correct ETA + //cout << "eta is " << t->eta << ", rwy is " << (rwyList.size() ? "occupied " : "clear ") << '\n'; + Point3D tortho = ortho.ConvertToLocal(t->pos); + if(t->eta < 12 && rwyList.size() && !(t->instructedToGoAround)) { + // TODO - need to make this more sophisticated + // eg. is the plane accelerating down the runway taking off [OK], + // or stationary near the start [V. BAD!!]. + // For now this should stop the AI plane landing on top of the user. + tower_plane_rec_list_iterator twrItr; + twrItr = rwyList.begin(); + TowerPlaneRec* tpr = *twrItr; + if(strcmp ( tpr->plane.callsign.c_str(), t->plane.callsign.c_str() ) == 0 && rwyList.size() == 1) { + // Fixing bug when ATC says that we must go around because of traffic on rwy + // but that traffic is we! In future we can use this expression + // for other ATC-messages like "On ground at 46, vacate left." + + } else { + string trns = t->plane.callsign; + trns += " GO AROUND TRAFFIC ON RUNWAY I REPEAT GO AROUND"; + pending_transmission = trns; + ImmediateTransmit(); + t->instructedToGoAround = true; + t->clearedToLand = false; + t->nextOnRwy = false; // But note this is recalculated so don't rely on it + // Assume it complies!!! + t->opType = CIRCUIT; + t->leg = CLIMBOUT; + if(!t->isUser) { + if(t->planePtr) { + //cout << "Registering Go-around transmission with AI plane\n"; + t->planePtr->RegisterTransmission(13); + } + } else { + // TODO - add Go-around ack to comm options, + // remove report rwy vacated. (possibly). + } + } + } else if(t->isUser && t->eta < 90 && tortho.y() > -2500 && t->clearedToLand && t->gearUpReported == false) { + // Check if gear up or down + double gp = fgGetFloat("/gear/gear/position-norm"); + if(gp < 1) { + string trnsm = t->plane.callsign; + sg_srandom_time(); + int rnd = int(sg_random() * 2) + 1; + if(rnd == 2) { // Random message for more realistic ATC ;) + trnsm += ", LANDING GEAR APPEARS UP!"; + } else { + trnsm += ", Check wheels down and locked."; + } + pending_transmission = trnsm; + ImmediateTransmit(); + t->gearUpReported = true; + } + } else if(t->eta < 90 && !t->clearedToLand) { + //doThresholdETACalc(); + doThresholdUseOrder(); + // The whip through the appList is a hack since currently t->nextOnRwy doesn't always work + // TODO - fix this! + bool cf = false; + for(tower_plane_rec_list_iterator twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + if((*twrItr)->eta < t->eta) { + cf = true; + } + } + if(t->nextOnRwy && !cf) { + if(!rwyList.size()) { + string trns = t->plane.callsign; + trns += " Cleared to land"; + pending_transmission = trns; + Transmit(); + //if(t->isUser) cout << "Transmitting cleared to Land!!!\n"; + t->clearedToLand = true; + if(!t->isUser) { + t->planePtr->RegisterTransmission(7); + } + } + } else { + //if(t->isUser) cout << "Not next\n"; + } + } + + // Check for landing... + bool landed = false; + if(!t->isUser) { + if(t->planePtr) { + if(t->planePtr->GetLeg() == LANDING_ROLL) { + landed = true; + } + } else { + SG_LOG(SG_ATC, SG_ALERT, "WARNING - not user and null planePtr in CheckApproachList!"); + } + } else { // user + if(OnActiveRunway(t->pos)) { + landed = true; + } + } + + if(landed) { + // Duplicated in CheckCircuitList - must be able to rationalise this somehow! + //cout << "A " << t->plane.callsign << " has landed, adding to rwyList...\n"; + rwyList.push_front(t); + // TODO - if(!clearedToLand) shout something!! + t->clearedToLand = false; + RemoveFromTrafficList(t->plane.callsign); + //if(t->isUser) { + // t->opType = TTT_UNKNOWN; + //} // TODO - allow the user to specify opType via ATC menu + appListItr = appList.erase(appListItr); + if(appListItr == appList.end() ) { + appListItr = appList.begin(); + } + if (appList.empty()) + return; + + } + + ++appListItr; + } + //cout << "Done" << endl; +} + +// Do one plane from the departure list +void FGTower::CheckDepartureList(double dt) { + if(!depList.empty()) { + if(depListItr == depList.end()) { + depListItr = depList.begin(); + } + TowerPlaneRec* t = *depListItr; + //cout << "Dep list, checking " << t->plane.callsign; + + double distout; // meters + if(t->isUser) distout = dclGetHorizontalSeparation(Point3D(lon, lat, elev), Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), user_elev_node->getDoubleValue())); + else distout = dclGetHorizontalSeparation(Point3D(lon, lat, elev), t->planePtr->GetPos()); + //cout << " distout = " << distout << '\n'; + if(t->isUser && !(t->clearedToTakeOff)) { // HACK - we use clearedToTakeOff to check if ATC already contacted with plane (and cleared take-off) or not + if(!OnAnyRunway(Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), 0.0), false)) { + current_atcdialog->remove_entry(ident, USER_REQUEST_TAKE_OFF, TOWER); + t->clearedToTakeOff = true; // FIXME + } + } + if(distout > 10000) { + string trns = t->plane.callsign; + trns += " You are now clear of my airspace, good day"; + pending_transmission = trns; + Transmit(); + if(t->isUser) { + // Change the communication options + RemoveAllUserDialogOptions(); + //cout << "ADD A\n"; + current_atcdialog->add_entry(ident, "@AP Tower, @CS @MI miles @CD of the airport for full stop@AT", "Contact tower for VFR arrival (full stop)", TOWER, (int)USER_REQUEST_VFR_ARRIVAL_FULL_STOP); + } else { + // Send a clear-of-airspace signal + // TODO - implement this once we actually have departing AI traffic (currently all circuits or arrivals). + } + RemovePlane(t->plane.callsign); + } else { + ++depListItr; + } + } +} + +// ********** End periodic check functions *********************************************** +// *************************************************************************************** + + +// Remove all dialog options for this tower. +void FGTower::RemoveAllUserDialogOptions() { + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_DEPARTURE, TOWER); + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_ARRIVAL, TOWER); + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_ARRIVAL_FULL_STOP, TOWER); + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_ARRIVAL_TOUCH_AND_GO, TOWER); + current_atcdialog->remove_entry(ident, USER_REPORT_3_MILE_FINAL, TOWER); + current_atcdialog->remove_entry(ident, USER_REPORT_DOWNWIND, TOWER); + current_atcdialog->remove_entry(ident, USER_REPORT_RWY_VACATED, TOWER); + current_atcdialog->remove_entry(ident, USER_REPORT_GOING_AROUND, TOWER); + current_atcdialog->remove_entry(ident, USER_REQUEST_TAKE_OFF, TOWER); +} + +// Returns true if positions of crosswind/downwind/base leg turns should be constrained by previous traffic +// plus the constraint position as a rwy orientated orthopos (meters) +bool FGTower::GetCrosswindConstraint(double& cpos) { + if(crosswind_leg_pos != 0.0) { + cpos = crosswind_leg_pos; + return(true); + } else { + cpos = 0.0; + return(false); + } +} +bool FGTower::GetDownwindConstraint(double& dpos) { + if(fabs(downwind_leg_pos) > nominal_downwind_leg_pos) { + dpos = downwind_leg_pos; + return(true); + } else { + dpos = 0.0; + return(false); + } +} +bool FGTower::GetBaseConstraint(double& bpos) { + if(base_leg_pos < nominal_base_leg_pos) { + bpos = base_leg_pos; + return(true); + } else { + bpos = nominal_base_leg_pos; + return(false); + } +} + + +// Figure out which runways are active. +// For now we'll just be simple and do one active runway - eventually this will get much more complex +// This is a private function - public interface to the results of this is through GetActiveRunway +void FGTower::DoRwyDetails() { + //cout << "GetRwyDetails called" << endl; + + // Based on the airport-id and wind get the active runway + + //wind + double hdg = wind_from_hdg->getDoubleValue(); + double speed = wind_speed_knots->getDoubleValue(); + hdg = (speed == 0.0 ? 270.0 : hdg); + //cout << "Heading = " << hdg << '\n'; + + FGRunway runway; + bool rwyGood = globals->get_runways()->search(ident, int(hdg), &runway); + if(rwyGood) { + //cout << "RUNWAY GOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOD\n"; + activeRwy = runway._rwy_no; + rwy.rwyID = runway._rwy_no; + SG_LOG(SG_ATC, SG_INFO, "Active runway for airport " << ident << " is " << activeRwy); + + // Get the threshold position + double other_way = runway._heading - 180.0; + while(other_way <= 0.0) { + other_way += 360.0; + } + // move to the +l end/center of the runway + //cout << "Runway center is at " << runway._lon << ", " << runway._lat << '\n'; + Point3D origin = Point3D(runway._lon, runway._lat, aptElev); + Point3D ref = origin; + double tshlon, tshlat, tshr; + double tolon, tolat, tor; + rwy.length = runway._length * SG_FEET_TO_METER; + rwy.width = runway._width * SG_FEET_TO_METER; + geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), other_way, + rwy.length / 2.0 - 25.0, &tshlat, &tshlon, &tshr ); + geo_direct_wgs_84 ( aptElev, ref.lat(), ref.lon(), runway._heading, + rwy.length / 2.0 - 25.0, &tolat, &tolon, &tor ); + // Note - 25 meters in from the runway end is a bit of a hack to put the plane ahead of the user. + // now copy what we need out of runway into rwy + rwy.threshold_pos = Point3D(tshlon, tshlat, aptElev); + Point3D takeoff_end = Point3D(tolon, tolat, aptElev); + //cout << "Threshold position = " << tshlon << ", " << tshlat << ", " << aptElev << '\n'; + //cout << "Takeoff position = " << tolon << ", " << tolat << ", " << aptElev << '\n'; + rwy.hdg = runway._heading; + // Set the projection for the local area based on this active runway + ortho.Init(rwy.threshold_pos, rwy.hdg); + rwy.end1ortho = ortho.ConvertToLocal(rwy.threshold_pos); // should come out as zero + rwy.end2ortho = ortho.ConvertToLocal(takeoff_end); + + // Set the pattern direction + // TODO - we'll check for a facilities file with this in eventually - for now assume left traffic except + // for certain circumstances (RH parallel rwy). + rwy.patternDirection = -1; // Left + if(rwy.rwyID.size() == 3) { + rwy.patternDirection = (rwy.rwyID.substr(2,1) == "R" ? 1 : -1); + } + //cout << "Doing details, rwy.patterDirection is " << rwy.patternDirection << '\n'; + } else { + SG_LOG(SG_ATC, SG_ALERT, "Help - can't get good runway in FGTower!!"); + activeRwy = "NN"; + } +} + + +// Figure out if a given position lies on the active runway +// Might have to change when we consider more than one active rwy. +bool FGTower::OnActiveRunway(const Point3D& pt) { + // TODO - check that the centre calculation below isn't confused by displaced thesholds etc. + Point3D xyc((rwy.end1ortho.x() + rwy.end2ortho.x())/2.0, (rwy.end1ortho.y() + rwy.end2ortho.y())/2.0, 0.0); + Point3D xyp = ortho.ConvertToLocal(pt); + + //cout << "Length offset = " << fabs(xyp.y() - xyc.y()) << '\n'; + //cout << "Width offset = " << fabs(xyp.x() - xyc.x()) << '\n'; + + double rlen = rwy.length/2.0 + 5.0; + double rwidth = rwy.width/2.0; + double ldiff = fabs(xyp.y() - xyc.y()); + double wdiff = fabs(xyp.x() - xyc.x()); + + return((ldiff < rlen) && (wdiff < rwidth)); +} + +// Figure out if a given position lies on any runway or not +// Only call this at startup - reading the runways database is expensive and needs to be fixed! +bool FGTower::OnAnyRunway(const Point3D& pt, bool onGround) { + ATCData ad; + double dist = current_commlist->FindClosest(lon, lat, elev, ad, TOWER, 7.0); + if(dist < 0.0) { + return(false); + } + + // Based on the airport-id, go through all the runways and check for a point in them + + // TODO - do we actually need to search for the airport - surely we already know our ident and + // can just search runways of our airport??? + //cout << "Airport ident is " << ad.ident << '\n'; + FGRunway runway; + bool rwyGood = globals->get_runways()->search(ad.ident, &runway); + if(!rwyGood) { + SG_LOG(SG_ATC, SG_WARN, "Unable to find any runways for airport ID " << ad.ident << " in FGTower"); + } + bool on = false; + while(runway._id == ad.ident) { + on = OnRunway(pt, runway); + //cout << "Runway " << runway._rwy_no << ": On = " << (on ? "true\n" : "false\n"); + if(on) { + if(onGround == false) + return(true); + if(runway._rwy_no != "xx") + return(true); + } + globals->get_runways()->next(&runway); + } + return(on); +} + + +// Returns true if successful +bool FGTower::RemoveFromTrafficList(const string& id) { + tower_plane_rec_list_iterator twrItr; + for(twrItr = trafficList.begin(); twrItr != trafficList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + if(tpr->plane.callsign == id) { + trafficList.erase(twrItr); + trafficListItr = trafficList.begin(); + return(true); + } + } + SG_LOG(SG_ATC, SG_WARN, "Warning - unable to remove aircraft " << id << " from trafficList in FGTower"); + return(false); +} + + +// Returns true if successful +bool FGTower::RemoveFromAppList(const string& id) { + tower_plane_rec_list_iterator twrItr; + for(twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + if(tpr->plane.callsign == id) { + appList.erase(twrItr); + appListItr = appList.begin(); + return(true); + } + } + //SG_LOG(SG_ATC, SG_WARN, "Warning - unable to remove aircraft " << id << " from appList in FGTower"); + return(false); +} + +// Returns true if successful +bool FGTower::RemoveFromRwyList(const string& id) { + tower_plane_rec_list_iterator twrItr; + for(twrItr = rwyList.begin(); twrItr != rwyList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + if(tpr->plane.callsign == id) { + rwyList.erase(twrItr); + rwyListItr = rwyList.begin(); + return(true); + } + } + //SG_LOG(SG_ATC, SG_WARN, "Warning - unable to remove aircraft " << id << " from rwyList in FGTower"); + return(false); +} + + +// Add a tower plane rec with ETA to the traffic list in the correct position ETA-wise +// and set nextOnRwy if so. +// Returns true if this could cause a threshold ETA conflict with other traffic, false otherwise. +// For planes holding they are put in the first position with time to go, and the return value is +// true if in the first position (nextOnRwy) and false otherwise. +// See the comments in FGTower::doThresholdUseOrder for notes on the ordering +bool FGTower::AddToTrafficList(TowerPlaneRec* t, bool holding) { + //cout << "ADD: " << trafficList.size(); + //cout << "AddToTrafficList called, currently size = " << trafficList.size() << ", holding = " << holding << endl; + double separation_time = 90.0; // seconds - this is currently a guess for light plane separation, and includes a few seconds for a holding plane to taxi onto the rwy. + double departure_sep_time = 60.0; // Separation time behind departing airplanes. Comments above also apply. + bool conflict = false; + double lastETA = 0.0; + bool firstTime = true; + // FIXME - make this more robust for different plane types eg. light following heavy. + tower_plane_rec_list_iterator twrItr; + //twrItr = trafficList.begin(); + //while(1) { + for(twrItr = trafficList.begin(); twrItr != trafficList.end(); twrItr++) { + //if(twrItr == trafficList.end()) { + // cout << " END "; + // trafficList.push_back(t); + // return(holding ? firstTime : conflict); + //} else { + TowerPlaneRec* tpr = *twrItr; + if(holding) { + //cout << (tpr->isUser ? "USER!\n" : "NOT user\n"); + //cout << "tpr->eta - lastETA = " << tpr->eta - lastETA << '\n'; + double dep_allowance = (timeSinceLastDeparture < departure_sep_time ? departure_sep_time - timeSinceLastDeparture : 0.0); + double slot_time = (firstTime ? separation_time + dep_allowance : separation_time + departure_sep_time); + // separation_time + departure_sep_time in the above accounts for the fact that the arrival could be touch and go, + // and if not needs time to clear the rwy anyway. + if(tpr->eta - lastETA > slot_time) { + t->nextOnRwy = firstTime; + trafficList.insert(twrItr, t); + //cout << "\tH\t" << trafficList.size() << '\n'; + return(firstTime); + } + firstTime = false; + } else { + if(t->eta < tpr->eta) { + // Ugg - this one's tricky. + // It depends on what the two planes are doing and whether there's a conflict what we do. + if(tpr->eta - t->eta > separation_time) { // No probs, plane 2 can squeeze in before plane 1 with no apparent conflict + if(tpr->nextOnRwy) { + tpr->nextOnRwy = false; + t->nextOnRwy = true; + } + trafficList.insert(twrItr, t); + } else { // Ooops - this ones tricky - we have a potential conflict! + conflict = true; + // HACK - just add anyway for now and flag conflict - TODO - FIX THIS using CIRCUIT/STRAIGHT_IN and VFR/IFR precedence rules. + if(tpr->nextOnRwy) { + tpr->nextOnRwy = false; + t->nextOnRwy = true; + } + trafficList.insert(twrItr, t); + } + //cout << "\tC\t" << trafficList.size() << '\n'; + return(conflict); + } + } + //} + //++twrItr; + } + // If we get here we must be at the end of the list, or maybe the list is empty. + if(!trafficList.size()) { + t->nextOnRwy = true; + // conflict and firstTime should be false and true respectively in this case anyway. + } else { + t->nextOnRwy = false; + } + trafficList.push_back(t); + //cout << "\tE\t" << trafficList.size() << endl; + return(holding ? firstTime : conflict); +} + +// Add a tower plane rec with ETA to the circuit list in the correct position ETA-wise +// Returns true if this might cause a separation conflict (based on ETA) with other traffic, false otherwise. +// Safe to add a plane that is already in - planes with the same callsign are not added. +bool FGTower::AddToCircuitList(TowerPlaneRec* t) { + if(!t) { + //cout << "**********************************************\n"; + //cout << "AddToCircuitList called with NULL pointer!!!!!\n"; + //cout << "**********************************************\n"; + return false; + } + //cout << "ADD: " << circuitList.size(); + //cout << ident << " AddToCircuitList called for " << t->plane.callsign << ", currently size = " << circuitList.size() << endl; + double separation_time = 60.0; // seconds - this is currently a guess for light plane separation, and includes a few seconds for a holding plane to taxi onto the rwy. + bool conflict = false; + tower_plane_rec_list_iterator twrItr; + // First check if the plane is already in the list + //cout << "A" << endl; + //cout << "Checking whether " << t->plane.callsign << " is already in circuit list..." << endl; + //cout << "B" << endl; + for(twrItr = circuitList.begin(); twrItr != circuitList.end(); twrItr++) { + if((*twrItr)->plane.callsign == t->plane.callsign) { + //cout << "In list - returning...\n"; + return false; + } + } + //cout << "Not in list - adding..." << endl; + + for(twrItr = circuitList.begin(); twrItr != circuitList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + //cout << tpr->plane.callsign << " eta is " << tpr->eta << '\n'; + //cout << "New eta is " << t->eta << '\n'; + if(t->eta < tpr->eta) { + // Ugg - this one's tricky. + // It depends on what the two planes are doing and whether there's a conflict what we do. + if(tpr->eta - t->eta > separation_time) { // No probs, plane 2 can squeeze in before plane 1 with no apparent conflict + circuitList.insert(twrItr, t); + circuitListItr = circuitList.begin(); + } else { // Ooops - this ones tricky - we have a potential conflict! + conflict = true; + // HACK - just add anyway for now and flag conflict. + circuitList.insert(twrItr, t); + circuitListItr = circuitList.begin(); + } + //cout << "\tC\t" << circuitList.size() << '\n'; + return(conflict); + } + } + // If we get here we must be at the end of the list, or maybe the list is empty. + //cout << ident << " adding " << t->plane.callsign << " to circuitList" << endl; + circuitList.push_back(t); // TODO - check the separation with the preceding plane for the conflict flag. + circuitListItr = circuitList.begin(); + //cout << "\tE\t" << circuitList.size() << endl; + return(conflict); +} + +// Add to vacated list only if not already present +void FGTower::AddToVacatedList(TowerPlaneRec* t) { + tower_plane_rec_list_iterator twrItr; + bool found = false; + for(twrItr = vacatedList.begin(); twrItr != vacatedList.end(); twrItr++) { + if((*twrItr)->plane.callsign == t->plane.callsign) { + found = true; + } + } + if(found) return; + vacatedList.push_back(t); +} + +void FGTower::AddToHoldingList(TowerPlaneRec* t) { + tower_plane_rec_list_iterator it, end = holdList.end(); + for (it = holdList.begin(); it != end; ++it) { + if ((*it)->plane.callsign == t->plane.callsign) + return; + + holdList.push_back(t); + } +} + +// Calculate the eta of a plane to the threshold. +// For ground traffic this is the fastest they can get there. +// For air traffic this is the middle approximation. +void FGTower::CalcETA(TowerPlaneRec* tpr, bool printout) { + // For now we'll be very crude and hardwire expected speeds to C172-like values + // The speeds below are specified in knots IAS and then converted to m/s + double app_ias = 100.0 * 0.514444; // Speed during straight-in approach + double circuit_ias = 80.0 * 0.514444; // Speed around circuit + double final_ias = 70.0 * 0.514444; // Speed during final approach + + //if(printout) { + //cout << "In CalcETA, airplane ident = " << tpr->plane.callsign << '\n'; + //cout << (tpr->isUser ? "USER\n" : "AI\n"); + //cout << flush; + //} + + // Sign convention - dist_out is -ve for approaching planes and +ve for departing planes + // dist_across is +ve in the pattern direction - ie a plane correctly on downwind will have a +ve dist_across + + Point3D op = ortho.ConvertToLocal(tpr->pos); + //if(printout) { + //if(!tpr->isUser) cout << "Orthopos is " << op.x() << ", " << op.y() << ' '; + //cout << "opType is " << tpr->opType << '\n'; + //} + double dist_out_m = op.y(); + double dist_across_m = fabs(op.x()); // The fabs is a hack to cope with the fact that we don't know the circuit direction yet + //cout << "Doing ETA calc for " << tpr->plane.callsign << '\n'; + + if(tpr->opType == STRAIGHT_IN || tpr->opType == INBOUND) { + //cout << "CASE 1\n"; + double dist_to_go_m = sqrt((dist_out_m * dist_out_m) + (dist_across_m * dist_across_m)); + if(dist_to_go_m < 1000) { + tpr->eta = dist_to_go_m / final_ias; + } else { + tpr->eta = (1000.0 / final_ias) + ((dist_to_go_m - 1000.0) / app_ias); + } + } else if(tpr->opType == CIRCUIT || tpr->opType == TTT_UNKNOWN) { // Hack alert - UNKNOWN has sort of been added here as a temporary hack. + //cout << "CASE 2\n"; + // It's complicated - depends on if base leg is delayed or not + //if(printout) { + //cout << "Leg = " << tpr->leg << '\n'; + //} + if(tpr->leg == LANDING_ROLL) { + tpr->eta = 0; + } else if((tpr->leg == FINAL) || (tpr->leg == TURN4)) { + //cout << "dist_out_m = " << dist_out_m << '\n'; + tpr->eta = fabs(dist_out_m) / final_ias; + } else if((tpr->leg == BASE) || (tpr->leg == TURN3)) { + tpr->eta = (fabs(dist_out_m) / final_ias) + (dist_across_m / circuit_ias); + } else { + // Need to calculate where base leg is likely to be + // FIXME - for now I'll hardwire it to 1000m which is what AILocalTraffic uses!!! + // TODO - as a matter of design - AILocalTraffic should get the nominal no-traffic base turn distance from Tower, since in real life the published pattern might differ from airport to airport + double nominal_base_dist_out_m = -1000; + double current_base_dist_out_m; + if(!GetBaseConstraint(current_base_dist_out_m)) { + current_base_dist_out_m = nominal_base_dist_out_m; + } + //cout << "current_base_dist_out_m = " << current_base_dist_out_m << '\n'; + double nominal_dist_across_m = 1000; // Hardwired value from AILocalTraffic + double current_dist_across_m; + if(!GetDownwindConstraint(current_dist_across_m)) { + current_dist_across_m = nominal_dist_across_m; + } + double nominal_cross_dist_out_m = 2000; // Bit of a guess - AI plane turns to crosswind at 700ft agl. + tpr->eta = fabs(current_base_dist_out_m) / final_ias; // final + //cout << "a = " << tpr->eta << '\n'; + if((tpr->leg == DOWNWIND) || (tpr->leg == TURN2)) { + tpr->eta += dist_across_m / circuit_ias; + //cout << "b = " << tpr->eta << '\n'; + tpr->eta += fabs(current_base_dist_out_m - dist_out_m) / circuit_ias; + //cout << "c = " << tpr->eta << '\n'; + } else if((tpr->leg == CROSSWIND) || (tpr->leg == TURN1)) { + //cout << "CROSSWIND calc: "; + //cout << tpr->eta << ' '; + if(dist_across_m > nominal_dist_across_m) { + tpr->eta += dist_across_m / circuit_ias; + //cout << "a "; + } else { + tpr->eta += nominal_dist_across_m / circuit_ias; + //cout << "b "; + } + //cout << tpr->eta << ' '; + // should we use the dist across of the previous plane if there is previous still on downwind? + //if(printout) cout << "bb = " << tpr->eta << '\n'; + if(dist_out_m > nominal_cross_dist_out_m) { + tpr->eta += fabs(current_base_dist_out_m - dist_out_m) / circuit_ias; + //cout << "c "; + } else { + tpr->eta += fabs(current_base_dist_out_m - nominal_cross_dist_out_m) / circuit_ias; + //cout << "d "; + } + //cout << tpr->eta << ' '; + //if(printout) cout << "cc = " << tpr->eta << '\n'; + if(nominal_dist_across_m > dist_across_m) { + tpr->eta += (nominal_dist_across_m - dist_across_m) / circuit_ias; + //cout << "e "; + } else { + // Nothing to add + //cout << "f "; + } + //cout << tpr->eta << '\n'; + //if(printout) cout << "dd = " << tpr->eta << '\n'; + } else { + // We've only just started - why not use a generic estimate? + tpr->eta = 240.0; + } + } + //if(printout) { + // cout << "ETA = " << tpr->eta << '\n'; + //} + //if(!tpr->isUser) cout << tpr->plane.callsign << '\t' << tpr->eta << '\n'; + } else { + tpr->eta = 99999; + } +} + + +// Calculate the distance of a plane to the threshold in meters +// TODO - Modify to calculate flying distance of a plane in the circuit +double FGTower::CalcDistOutM(TowerPlaneRec* tpr) { + return(dclGetHorizontalSeparation(rwy.threshold_pos, tpr->pos)); +} + + +// Calculate the distance of a plane to the threshold in miles +// TODO - Modify to calculate flying distance of a plane in the circuit +double FGTower::CalcDistOutMiles(TowerPlaneRec* tpr) { + return(CalcDistOutM(tpr) / 1600.0); // FIXME - use a proper constant if possible. +} + + +// Iterate through all the lists, update the position of, and call CalcETA for all the planes. +void FGTower::doThresholdETACalc() { + //cout << "Entering doThresholdETACalc..." << endl; + tower_plane_rec_list_iterator twrItr; + // Do the approach list first + for(twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + if(!(tpr->isUser)) tpr->pos = tpr->planePtr->GetPos(); + //cout << "APP: "; + CalcETA(tpr); + } + // Then the circuit list + //cout << "Circuit list size is " << circuitList.size() << '\n'; + for(twrItr = circuitList.begin(); twrItr != circuitList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + if(!(tpr->isUser)) tpr->pos = tpr->planePtr->GetPos(); + //cout << "CIRC: "; + CalcETA(tpr); + } + //cout << "Done doThresholdETCCalc" << endl; +} + + +// Check that the planes in traffic list are correctly ordered, +// that the nearest (timewise) is flagged next on rwy, and return +// true if any threshold use conflicts are detected, false otherwise. +bool FGTower::doThresholdUseOrder() { + //cout << "Entering doThresholdUseOrder..." << endl; + bool conflict = false; + + // Wipe out traffic list, go through circuit, app and hold list, and reorder them in traffic list. + // Here's the rather simplistic assumptions we're using: + // Currently all planes are assumed to be GA light singles with corresponding speeds and separation times. + // In order of priority for runway use: + // STRAIGHT_IN > CIRCUIT > HOLDING_FOR_DEPARTURE + // No modification of planes speeds occurs - conflicts are resolved by delaying turn for base, + // and holding planes until a space. + // When calculating if a holding plane can use the runway, time clearance from last departure + // as well as time clearance to next arrival must be considered. + + trafficList.clear(); + + tower_plane_rec_list_iterator twrItr; + // Do the approach list first + //if(ident == "KRHV") cout << "A" << flush; + for(twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + //if(ident == "KRHV") cout << tpr->plane.callsign << '\n'; + conflict = AddToTrafficList(tpr); + } + // Then the circuit list + //if(ident == "KRHV") cout << "C" << flush; + for(twrItr = circuitList.begin(); twrItr != circuitList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + //if(ident == "KRHV") cout << tpr->plane.callsign << '\n'; + conflict = AddToTrafficList(tpr); + } + // And finally the hold list + //cout << "H" << endl; + for(twrItr = holdList.begin(); twrItr != holdList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + AddToTrafficList(tpr, true); + } + + + if(0) { + //if(ident == "KRHV") { + cout << "T\n"; + for(twrItr = trafficList.begin(); twrItr != trafficList.end(); twrItr++) { + TowerPlaneRec* tpr = *twrItr; + cout << tpr->plane.callsign << '\t' << tpr->eta << '\t'; + } + cout << endl; + } + + //cout << "Done doThresholdUseOrder" << endl; + return(conflict); +} + + +// Return the ETA of plane no. list_pos (1-based) in the traffic list. +// i.e. list_pos = 1 implies next to use runway. +double FGTower::GetTrafficETA(unsigned int list_pos, bool printout) { + if(trafficList.size() < list_pos) { + return(99999); + } + + tower_plane_rec_list_iterator twrItr; + twrItr = trafficList.begin(); + for(unsigned int i = 1; i < list_pos; i++, twrItr++); + TowerPlaneRec* tpr = *twrItr; + CalcETA(tpr, printout); + //cout << "ETA returned = " << tpr->eta << '\n'; + return(tpr->eta); +} + + +void FGTower::ContactAtHoldShort(const PlaneRec& plane, FGAIPlane* requestee, tower_traffic_type operation) { + // HACK - assume that anything contacting at hold short is new for now - FIXME LATER + TowerPlaneRec* t = new TowerPlaneRec; + t->plane = plane; + t->planePtr = requestee; + t->holdShortReported = true; + t->clearedToLineUp = false; + t->clearedToTakeOff = false; + t->opType = operation; + t->pos = requestee->GetPos(); + + //cout << "Hold Short reported by " << plane.callsign << '\n'; + SG_LOG(SG_ATC, SG_BULK, "Hold Short reported by " << plane.callsign); + +/* + bool next = AddToTrafficList(t, true); + if(next) { + double teta = GetTrafficETA(2); + if(teta < 150.0) { + t->clearanceCounter = 7.0; // This reduces the delay before response to 3 secs if an immediate takeoff is reqd + //cout << "Reducing response time to request due imminent traffic\n"; + } + } else { + } +*/ + // TODO - possibly add the reduced interval to clearance when immediate back in under the new scheme + + holdList.push_back(t); + + responseReqd = true; +} + +// Register the presence of an AI plane at a point where contact would already have been made in real life +// CAUTION - currently it is assumed that this plane's callsign is unique - it is up to AIMgr to generate unique callsigns. +void FGTower::RegisterAIPlane(const PlaneRec& plane, FGAIPlane* ai, const tower_traffic_type& op, const PatternLeg& lg) { + // At the moment this is only going to be tested with inserting an AI plane on downwind + TowerPlaneRec* t = new TowerPlaneRec; + t->plane = plane; + t->planePtr = ai; + t->opType = op; + t->leg = lg; + t->pos = ai->GetPos(); + + CalcETA(t); + + if(op == CIRCUIT && lg != LEG_UNKNOWN) { + AddToCircuitList(t); + } else { + // FLAG A WARNING + } + + doThresholdUseOrder(); +} + +void FGTower::DeregisterAIPlane(const string& id) { + RemovePlane(id); +} + +// Contact tower for VFR approach +// eg "Cessna Charlie Foxtrot Golf Foxtrot Sierra eight miles South of the airport for full stop with Bravo" +// This function probably only called via user interaction - AI planes will have an overloaded function taking a planerec. +// opt defaults to AIP_LT_UNKNOWN +void FGTower::VFRArrivalContact(const string& ID, const LandingType& opt) { + //cout << "USER Request Landing Clearance called for ID " << ID << '\n'; + + // For now we'll assume that the user is a light plane and can get him/her to join the circuit if necessary. + + TowerPlaneRec* t; + string usercall = fgGetString("/sim/user/callsign"); + if(ID == "USER" || ID == usercall) { + t = FindPlane(usercall); + if(!t) { + //cout << "NOT t\n"; + t = new TowerPlaneRec; + t->isUser = true; + t->pos.setlon(user_lon_node->getDoubleValue()); + t->pos.setlat(user_lat_node->getDoubleValue()); + t->pos.setelev(user_elev_node->getDoubleValue()); + } else { + //cout << "IS t\n"; + // Oops - the plane is already registered with this tower - maybe we took off and flew a giant circuit without + // quite getting out of tower airspace - just ignore for now and treat as new arrival. + // TODO - Maybe should remove from departure and circuit list if in there though!! + } + } else { + // Oops - something has gone wrong - put out a warning + cout << "WARNING - FGTower::VFRContact(string ID, LandingType lt) called with ID " << ID << " which does not appear to be the user.\n"; + return; + } + + + // TODO + // Calculate where the plane is in relation to the active runway and it's circuit + // and set the op-type as appropriate. + + // HACK - to get up and running I'm going to assume that the user contacts tower on a staight-in final for now. + t->opType = STRAIGHT_IN; + + t->plane.type = GA_SINGLE; // FIXME - Another assumption! + t->plane.callsign = usercall; + + t->vfrArrivalReported = true; + responseReqd = true; + + appList.push_back(t); // Not necessarily permanent + appListItr = appList.begin(); + AddToTrafficList(t); + + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_ARRIVAL, TOWER); + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_ARRIVAL_FULL_STOP, TOWER); + current_atcdialog->remove_entry(ident, USER_REQUEST_VFR_ARRIVAL_TOUCH_AND_GO, TOWER); +} + +// landingType defaults to AIP_LT_UNKNOWN +void FGTower::VFRArrivalContact(const PlaneRec& plane, FGAIPlane* requestee, const LandingType& lt) { + //cout << "VFRArrivalContact called for plane " << plane.callsign << " at " << ident << '\n'; + // Possible hack - assume this plane is new for now - TODO - should check really + TowerPlaneRec* t = new TowerPlaneRec; + t->plane = plane; + t->planePtr = requestee; + t->landingType = lt; + t->pos = requestee->GetPos(); + + //cout << "Hold Short reported by " << plane.callsign << '\n'; + SG_LOG(SG_ATC, SG_BULK, "VFR arrival contact made by " << plane.callsign); + //cout << "VFR arrival contact made by " << plane.callsign << '\n'; + + // HACK - to get up and running I'm going to assume a staight-in final for now. + t->opType = STRAIGHT_IN; + + t->vfrArrivalReported = true; + responseReqd = true; + + //cout << "Before adding, appList.size = " << appList.size() << " at " << ident << '\n'; + appList.push_back(t); // Not necessarily permanent + appListItr = appList.begin(); + //cout << "After adding, appList.size = " << appList.size() << " at " << ident << '\n'; + AddToTrafficList(t); +} + +void FGTower::RequestDepartureClearance(const string& ID) { + //cout << "Request Departure Clearance called...\n"; +} + +void FGTower::RequestTakeOffClearance(const string& ID) { + string uid=ID; + if(ID == "USER") { + uid = fgGetString("/sim/user/callsign"); + current_atcdialog->remove_entry(ident, USER_REQUEST_TAKE_OFF, TOWER); + } + TowerPlaneRec* t = FindPlane(uid); + if(t) { + if(!(t->clearedToTakeOff)) { + departed = false; + t->lineUpReported=true; + responseReqd = true; + } + } + else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unable to find plane " << ID << " in FGTower::RequestTakeOffClearance(...)"); + } +} + +void FGTower::ReportFinal(const string& ID) { + //cout << "Report Final Called at tower " << ident << " by plane " << ID << '\n'; + string uid=ID; + if(ID == "USER") { + uid = fgGetString("/sim/user/callsign"); + current_atcdialog->remove_entry(ident, USER_REPORT_3_MILE_FINAL, TOWER); + } + TowerPlaneRec* t = FindPlane(uid); + if(t) { + t->finalReported = true; + t->finalAcknowledged = false; + if(!(t->clearedToLand)) { + responseReqd = true; + } else { + // possibly respond with wind even if already cleared to land? + t->finalReported = false; + t->finalAcknowledged = true; + // HACK!! - prevents next reporting being misinterpreted as this one. + } + } else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unable to find plane " << ID << " in FGTower::ReportFinal(...)"); + } +} + +void FGTower::ReportLongFinal(const string& ID) { + string uid=ID; + if(ID == "USER") { + uid = fgGetString("/sim/user/callsign"); + current_atcdialog->remove_entry(ident, USER_REPORT_3_MILE_FINAL, TOWER); + } + TowerPlaneRec* t = FindPlane(uid); + if(t) { + t->longFinalReported = true; + t->longFinalAcknowledged = false; + if(!(t->clearedToLand)) { + responseReqd = true; + } // possibly respond with wind even if already cleared to land? + } else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unable to find plane " << ID << " in FGTower::ReportLongFinal(...)"); + } +} + +//void FGTower::ReportOuterMarker(string ID); +//void FGTower::ReportMiddleMarker(string ID); +//void FGTower::ReportInnerMarker(string ID); + +void FGTower::ReportRunwayVacated(const string& ID) { + //cout << "Report Runway Vacated Called at tower " << ident << " by plane " << ID << '\n'; + string uid=ID; + if(ID == "USER") { + uid = fgGetString("/sim/user/callsign"); + current_atcdialog->remove_entry(ident, USER_REPORT_RWY_VACATED, TOWER); + } + TowerPlaneRec* t = FindPlane(uid); + if(t) { + //cout << "Found it...\n"; + t->rwyVacatedReported = true; + responseReqd = true; + } else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unable to find plane " << ID << " in FGTower::ReportRunwayVacated(...)"); + SG_LOG(SG_ATC, SG_ALERT, "A WARNING: Unable to find plane " << ID << " in FGTower::ReportRunwayVacated(...)"); + //cout << "WARNING: Unable to find plane " << ID << " in FGTower::ReportRunwayVacated(...)\n"; + } +} + +TowerPlaneRec* FGTower::FindPlane(const string& ID) { + //cout << "FindPlane called for " << ID << "...\n"; + tower_plane_rec_list_iterator twrItr; + // Do the approach list first + for(twrItr = appList.begin(); twrItr != appList.end(); twrItr++) { + //cout << "appList callsign is " << (*twrItr)->plane.callsign << '\n'; + if((*twrItr)->plane.callsign == ID) return(*twrItr); + } + // Then the circuit list + for(twrItr = circuitList.begin(); twrItr != circuitList.end(); twrItr++) { + //cout << "circuitList callsign is " << (*twrItr)->plane.callsign << '\n'; + if((*twrItr)->plane.callsign == ID) return(*twrItr); + } + // Then the runway list + //cout << "rwyList.size() is " << rwyList.size() << '\n'; + for(twrItr = rwyList.begin(); twrItr != rwyList.end(); twrItr++) { + //cout << "rwyList callsign is " << (*twrItr)->plane.callsign << '\n'; + if((*twrItr)->plane.callsign == ID) return(*twrItr); + } + // The hold list + for(twrItr = holdList.begin(); twrItr != holdList.end(); twrItr++) { + if((*twrItr)->plane.callsign == ID) return(*twrItr); + } + // And finally the vacated list + for(twrItr = vacatedList.begin(); twrItr != vacatedList.end(); twrItr++) { + //cout << "vacatedList callsign is " << (*twrItr)->plane.callsign << '\n'; + if((*twrItr)->plane.callsign == ID) return(*twrItr); + } + SG_LOG(SG_ATC, SG_WARN, "Unable to find " << ID << " in FGTower::FindPlane(...)"); + //exit(-1); + return(NULL); +} + +void FGTower::RemovePlane(const string& ID) { + //cout << ident << " RemovePlane called for " << ID << '\n'; + // We have to be careful here - we want to erase the plane from all lists it is in, + // but we can only delete it once, AT THE END. + TowerPlaneRec* t = NULL; + tower_plane_rec_list_iterator twrItr; + for(twrItr = appList.begin(); twrItr != appList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = appList.erase(twrItr); + appListItr = appList.begin(); + // HACK: aircraft are sometimes more than once in a list, so we need to + // remove them all before we can delete the TowerPlaneRec class + //break; + } else + ++twrItr; + } + for(twrItr = depList.begin(); twrItr != depList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = depList.erase(twrItr); + depListItr = depList.begin(); + } else + ++twrItr; + } + for(twrItr = circuitList.begin(); twrItr != circuitList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = circuitList.erase(twrItr); + circuitListItr = circuitList.begin(); + } else + ++twrItr; + } + for(twrItr = holdList.begin(); twrItr != holdList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = holdList.erase(twrItr); + holdListItr = holdList.begin(); + } else + ++twrItr; + } + for(twrItr = rwyList.begin(); twrItr != rwyList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = rwyList.erase(twrItr); + rwyListItr = rwyList.begin(); + } else + ++twrItr; + } + for(twrItr = vacatedList.begin(); twrItr != vacatedList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = vacatedList.erase(twrItr); + vacatedListItr = vacatedList.begin(); + } else + ++twrItr; + } + for(twrItr = trafficList.begin(); twrItr != trafficList.end();) { + if((*twrItr)->plane.callsign == ID) { + t = *twrItr; + twrItr = trafficList.erase(twrItr); + trafficListItr = trafficList.begin(); + } else + ++twrItr; + } + // And finally, delete the record. + delete t; +} + +void FGTower::ReportDownwind(const string& ID) { + //cout << "ReportDownwind(...) called\n"; + string uid=ID; + if(ID == "USER") { + uid = fgGetString("/sim/user/callsign"); + current_atcdialog->remove_entry(ident, USER_REPORT_DOWNWIND, TOWER); + } + TowerPlaneRec* t = FindPlane(uid); + if(t) { + t->downwindReported = true; + responseReqd = true; + // If the plane is in the app list, remove it and put it in the circuit list instead. + // Ideally we might want to do this at the 2 mile report prior to 45 deg entry, but at + // the moment that would b&gg?r up the constraint position calculations. + RemoveFromAppList(ID); + t->leg = DOWNWIND; + if(t->isUser) { + t->pos.setlon(user_lon_node->getDoubleValue()); + t->pos.setlat(user_lat_node->getDoubleValue()); + t->pos.setelev(user_elev_node->getDoubleValue()); + } else { + // ASSERT(t->planePtr != NULL); + t->pos = t->planePtr->GetPos(); + } + CalcETA(t); + AddToCircuitList(t); + } else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unable to find plane " << ID << " in FGTower::ReportDownwind(...)"); + } +} + +void FGTower::ReportGoingAround(const string& ID) { + string uid=ID; + if(ID == "USER") { + uid = fgGetString("/sim/user/callsign"); + RemoveAllUserDialogOptions(); // TODO - it would be much more efficient if ATCDialog simply had a clear() function!!! + current_atcdialog->add_entry(ident, "@AP Tower, @CS Downwind @RW", "Report Downwind", TOWER, (int)USER_REPORT_DOWNWIND); + } + TowerPlaneRec* t = FindPlane(uid); + if(t) { + //t->goAroundReported = true; // No need to set this until we start responding to it. + responseReqd = false; // might change in the future but for now we'll not distract them during the go-around. + // If the plane is in the app list, remove it and put it in the circuit list instead. + RemoveFromAppList(ID); + t->leg = CLIMBOUT; + if(t->isUser) { + t->pos.setlon(user_lon_node->getDoubleValue()); + t->pos.setlat(user_lat_node->getDoubleValue()); + t->pos.setelev(user_elev_node->getDoubleValue()); + } else { + // ASSERT(t->planePtr != NULL); + t->pos = t->planePtr->GetPos(); + } + CalcETA(t); + AddToCircuitList(t); + } else { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Unable to find plane " << ID << " in FGTower::ReportDownwind(...)"); + } +} + +string FGTower::GenText(const string& m, int c) { + const int cmax = 300; + //string message; + char tag[4]; + char crej = '@'; + char mes[cmax]; + char dum[cmax]; + //char buf[10]; + char *pos; + int len; + //FGTransmission t; + string usercall = fgGetString("/sim/user/callsign"); + TowerPlaneRec* t = FindPlane(responseID); + + //transmission_list_type tmissions = transmissionlist_station[station]; + //transmission_list_iterator current = tmissions.begin(); + //transmission_list_iterator last = tmissions.end(); + + //for ( ; current != last ; ++current ) { + // if ( current->get_code().c1 == code.c1 && + // current->get_code().c2 == code.c2 && + // current->get_code().c3 == code.c3 ) { + + //if ( ttext ) message = current->get_transtext(); + //else message = current->get_menutext(); + strcpy( &mes[0], m.c_str() ); + + // Replace all the '@' parameters with the actual text. + int check = 0; // If mes gets overflowed the while loop can go infinite + double gp = fgGetFloat("/gear/gear/position-norm"); + while ( strchr(&mes[0], crej) != NULL ) { // ie. loop until no more occurances of crej ('@') found + pos = strchr( &mes[0], crej ); + memmove(&tag[0], pos, 3); + tag[3] = '\0'; + int i; + len = 0; + for ( i=0; i<cmax; i++ ) { + if ( mes[i] == crej ) { + len = i; + break; + } + } + strncpy( &dum[0], &mes[0], len ); + dum[len] = '\0'; + + if ( strcmp ( tag, "@ST" ) == 0 ) + //strcat( &dum[0], tpars.station.c_str() ); + strcat(&dum[0], ident.c_str()); + else if ( strcmp ( tag, "@AP" ) == 0 ) + //strcat( &dum[0], tpars.airport.c_str() ); + strcat(&dum[0], name.c_str()); + else if ( strcmp ( tag, "@CS" ) == 0 ) + //strcat( &dum[0], tpars.callsign.c_str() ); + strcat(&dum[0], usercall.c_str()); + else if ( strcmp ( tag, "@TD" ) == 0 ) { + /* + if ( tpars.tdir == 1 ) { + char buf[] = "left"; + strcat( &dum[0], &buf[0] ); + } + else { + char buf[] = "right"; + strcat( &dum[0], &buf[0] ); + } + */ + } + else if ( strcmp ( tag, "@HE" ) == 0 ) { + /* + char buf[10]; + sprintf( buf, "%i", (int)(tpars.heading) ); + strcat( &dum[0], &buf[0] ); + */ + } + else if ( strcmp ( tag, "@AT" ) == 0 ) { // ATIS ID + /* + char buf[10]; + sprintf( buf, "%i", (int)(tpars.heading) ); + strcat( &dum[0], &buf[0] ); + */ + double f = globals->get_ATC_mgr()->GetFrequency(ident, ATIS) / 100.0; + if(f) { + string atis_id; + atis_id = ", information " + GetATISID(); + strcat( &dum[0], atis_id.c_str() ); + } + } + else if ( strcmp ( tag, "@VD" ) == 0 ) { + /* + if ( tpars.VDir == 1 ) { + char buf[] = "Descend and maintain"; + strcat( &dum[0], &buf[0] ); + } + else if ( tpars.VDir == 2 ) { + char buf[] = "Maintain"; + strcat( &dum[0], &buf[0] ); + } + else if ( tpars.VDir == 3 ) { + char buf[] = "Climb and maintain"; + strcat( &dum[0], &buf[0] ); + } + */ + } + else if ( strcmp ( tag, "@AL" ) == 0 ) { + /* + char buf[10]; + sprintf( buf, "%i", (int)(tpars.alt) ); + strcat( &dum[0], &buf[0] ); + */ + } + else if ( strcmp ( tag, "@TO" ) == 0 ) { // Requesting take-off or departure clearance + string tmp; + if (rwyOccupied) { + tmp = "Ready for take-off"; + } else { + if (OnAnyRunway(Point3D(user_lon_node->getDoubleValue(), + user_lat_node->getDoubleValue(), 0.0),true)) { + tmp = "Request take-off clearance"; + } else { + tmp = "Request departure clearance"; + } + } + strcat(&dum[0], tmp.c_str()); + } + else if ( strcmp ( tag, "@MI" ) == 0 ) { + char buf[10]; + //sprintf( buf, "%3.1f", tpars.miles ); + int dist_miles = (int)dclGetHorizontalSeparation(Point3D(lon, lat, elev), Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), user_elev_node->getDoubleValue())) / 1600; + sprintf(buf, "%i", dist_miles); + strcat( &dum[0], &buf[0] ); + } + else if ( strcmp ( tag, "@FR" ) == 0 ) { + /* + char buf[10]; + sprintf( buf, "%6.2f", tpars.freq ); + strcat( &dum[0], &buf[0] ); + */ + } + else if ( strcmp ( tag, "@RW" ) == 0 ) { + strcat(&dum[0], ConvertRwyNumToSpokenString(activeRwy).c_str()); + } + else if ( strcmp ( tag, "@GR" ) == 0 ) { // Gear position (on final) + if(t->gearWasUp && gp > 0.99) { + strcat(&dum[0], ", gear down, ready to land."); + } + } + else if(strcmp(tag, "@CD") == 0) { // @CD = compass direction + double h = GetHeadingFromTo(Point3D(lon, lat, elev), Point3D(user_lon_node->getDoubleValue(), user_lat_node->getDoubleValue(), user_elev_node->getDoubleValue())); + while(h < 0.0) h += 360.0; + while(h > 360.0) h -= 360.0; + if(h < 22.5 || h > 337.5) { + strcat(&dum[0], "North"); + } else if(h < 67.5) { + strcat(&dum[0], "North-East"); + } else if(h < 112.5) { + strcat(&dum[0], "East"); + } else if(h < 157.5) { + strcat(&dum[0], "South-East"); + } else if(h < 202.5) { + strcat(&dum[0], "South"); + } else if(h < 247.5) { + strcat(&dum[0], "South-West"); + } else if(h < 292.5) { + strcat(&dum[0], "West"); + } else { + strcat(&dum[0], "North-West"); + } + } else { + cout << "Tag " << tag << " not found" << endl; + break; + } + strcat( &dum[0], &mes[len+3] ); + strcpy( &mes[0], &dum[0] ); + + ++check; + if(check > 10) { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Possibly endless loop terminated in FGTransmissionlist::gen_text(...)"); + break; + } + } + + //cout << mes << endl; + //break; + //} + //} + return mes[0] ? mes : "No transmission found"; +} + +string FGTower::GetWeather() { + std::ostringstream msg; + + // wind + double hdg = wind_from_hdg->getDoubleValue(); + double speed = wind_speed_knots->getDoubleValue(); + if (speed < 1) + msg << "wind calm"; + else + msg << "wind " << int(hdg) << " degrees at " << int(speed) << " knots"; + + // visibility + double visibility = fgGetDouble("/environment/visibility-m"); + if (visibility < 10000) + msg << ", visibility " << int(visibility / 1609) << " miles"; + + // pressure / altimeter + double pressure = fgGetDouble("/environment/pressure-sea-level-inhg"); + msg << ", QFE " << fixed << setprecision(2) << pressure << "."; + + return msg.str(); +} + +string FGTower::GetATISID() { + int hours = fgGetInt("/sim/time/utc/hour"); + int phonetic_id = current_commlist->GetCallSign(ident, hours, 0); + return GetPhoneticIdent(phonetic_id); +} + +ostream& operator << (ostream& os, tower_traffic_type ttt) { + switch(ttt) { + case(CIRCUIT): return(os << "CIRCUIT"); + case(INBOUND): return(os << "INBOUND"); + case(OUTBOUND): return(os << "OUTBOUND"); + case(TTT_UNKNOWN): return(os << "UNKNOWN"); + case(STRAIGHT_IN): return(os << "STRAIGHT_IN"); + } + return(os << "ERROR - Unknown switch in tower_traffic_type operator << "); +} + diff --git a/src/ATCDCL/tower.hxx b/src/ATCDCL/tower.hxx new file mode 100644 index 000000000..0fe4103a7 --- /dev/null +++ b/src/ATCDCL/tower.hxx @@ -0,0 +1,365 @@ +// FGTower - a class to provide tower control at towered airports. +// +// Written by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef _FG_TOWER_HXX +#define _FG_TOWER_HXX + +#include <simgear/compiler.h> +#include <simgear/math/point3d.hxx> +#include <simgear/misc/sgstream.hxx> +#include <plib/sg.h> + +#include STL_IOSTREAM +#include STL_STRING + +SG_USING_STD(string); +SG_USING_STD(ios); + +#include "ATC.hxx" +#include "ATCProjection.hxx" +#include "AIPlane.hxx" + +class FGATCMgr; +class FGGround; + +//DCL - a complete guess for now. +#define FG_TOWER_DEFAULT_RANGE 30 + +enum tower_traffic_type { + CIRCUIT, + INBOUND, // CIRCUIT traffic gets changed to INBOUND when on final of the full-stop circuit. + OUTBOUND, + TTT_UNKNOWN, // departure, but we don't know if for circuits or leaving properly + STRAIGHT_IN +}; + +ostream& operator << (ostream& os, tower_traffic_type ttt); + +enum tower_callback_type { + USER_REQUEST_VFR_DEPARTURE = 1, + USER_REQUEST_VFR_ARRIVAL = 2, + USER_REQUEST_VFR_ARRIVAL_FULL_STOP = 3, + USER_REQUEST_VFR_ARRIVAL_TOUCH_AND_GO = 4, + USER_REPORT_3_MILE_FINAL = 5, + USER_REPORT_DOWNWIND = 6, + USER_REPORT_RWY_VACATED = 7, + USER_REPORT_GOING_AROUND = 8, + USER_REQUEST_TAKE_OFF = 9 +}; + +// TODO - need some differentiation of IFR and VFR traffic in order to give the former priority. + +// Structure for holding details of a plane under tower control. +// Not fixed yet - may include more stuff later. +class TowerPlaneRec { + +public: + + TowerPlaneRec(); + TowerPlaneRec(const PlaneRec& p); + TowerPlaneRec(const Point3D& pt); + TowerPlaneRec(const PlaneRec& p, const Point3D& pt); + + FGAIPlane* planePtr; // This might move to the planeRec eventually + PlaneRec plane; + + Point3D pos; + double eta; // seconds + double dist_out; // meters from theshold + bool clearedToLand; + bool clearedToLineUp; + bool clearedToTakeOff; + // ought to add time cleared to depart so we can nag if necessary + bool holdShortReported; + bool lineUpReported; + bool downwindReported; + bool longFinalReported; + bool longFinalAcknowledged; + bool finalReported; + bool finalAcknowledged; + bool rwyVacatedReported; + bool rwyVacatedAcknowledged; + bool goAroundReported; // set true if plane informs tower that it's going around. + bool instructedToGoAround; // set true if plane told by tower to go around. + bool onRwy; // is physically on the runway + bool nextOnRwy; // currently projected by tower to be the next on the runway + bool gearWasUp; // Tell to ATC about gear + bool gearUpReported; // Tell to pilot about landing gear + + bool vfrArrivalReported; + bool vfrArrivalAcknowledged; + + // Type of operation the plane is doing + tower_traffic_type opType; + + // Whereabouts in circuit if doing circuits + PatternLeg leg; + + LandingType landingType; + bool isUser; // true if this plane is the user +}; + + +typedef list < TowerPlaneRec* > tower_plane_rec_list_type; +typedef tower_plane_rec_list_type::iterator tower_plane_rec_list_iterator; +typedef tower_plane_rec_list_type::const_iterator tower_plane_rec_list_const_iterator; + + +class FGTower : public FGATC { + +public: + + FGTower(); + ~FGTower(); + + void Init(); + + void Update(double dt); + + void ReceiveUserCallback(int code); + + // Contact tower for VFR approach + // eg "Cessna Charlie Foxtrot Golf Foxtrot Sierra eight miles South of the airport for full stop with Bravo" + // This function probably only called via user interaction - AI planes will have an overloaded function taking a planerec. + void VFRArrivalContact(const string& ID, const LandingType& opt = AIP_LT_UNKNOWN); + // For the AI planes... + void VFRArrivalContact(const PlaneRec& plane, FGAIPlane* requestee, const LandingType& lt = AIP_LT_UNKNOWN); + + void RequestDepartureClearance(const string& ID); + void RequestTakeOffClearance(const string& ID); + void ReportFinal(const string& ID); + void ReportLongFinal(const string& ID); + void ReportOuterMarker(const string& ID); + void ReportMiddleMarker(const string& ID); + void ReportInnerMarker(const string& ID); + void ReportRunwayVacated(const string& ID); + void ReportReadyForDeparture(const string& ID); + void ReportDownwind(const string& ID); + void ReportGoingAround(const string& ID); + + // Contact tower when at a hold short for departure - for now we'll assume plane - maybe vehicles might want to cross runway eventually? + void ContactAtHoldShort(const PlaneRec& plane, FGAIPlane* requestee, tower_traffic_type operation); + + // Register the presence of an AI plane at a point where contact would already have been made in real life + // CAUTION - currently it is assumed that this plane's callsign is unique - it is up to AIMgr to generate unique callsigns. + void RegisterAIPlane(const PlaneRec& plane, FGAIPlane* ai, const tower_traffic_type& op, const PatternLeg& lg = LEG_UNKNOWN); + + // Deregister and remove an AI plane. + void DeregisterAIPlane(const string& id); + + // Public interface to the active runway - this will get more complex + // in the future and consider multi-runway use, airplane weight etc. + inline const string& GetActiveRunway() const { return activeRwy; } + inline const RunwayDetails& GetActiveRunwayDetails() const { return rwy; } + // Get the pattern direction of the active rwy. + inline int GetPatternDirection() const { return rwy.patternDirection; } + + inline const string& get_trans_ident() const { return trans_ident; } + + inline FGGround* const GetGroundPtr() const { return ground; } + + // Returns true if positions of crosswind/downwind/base leg turns should be constrained by previous traffic + // plus the constraint position as a rwy orientated orthopos (meters) + bool GetCrosswindConstraint(double& cpos); + bool GetDownwindConstraint(double& dpos); + bool GetBaseConstraint(double& bpos); + + string GenText(const string& m, int c); + string GetWeather(); + string GetATISID(); + +private: + FGATCMgr* ATCmgr; + // This is purely for synactic convienience to avoid writing globals->get_ATC_mgr()-> all through the code! + + // Respond to a transmission + void Respond(); + + void ProcessRunwayVacatedReport(TowerPlaneRec* t); + void ProcessDownwindReport(TowerPlaneRec* t); + + // Remove all options from the user dialog choice + void RemoveAllUserDialogOptions(); + + // Periodic checks on the various traffic. + void CheckHoldList(double dt); + void CheckCircuitList(double dt); + void CheckRunwayList(double dt); + void CheckApproachList(double dt); + void CheckDepartureList(double dt); + + // Currently this assumes we *are* next on the runway and doesn't check for planes about to land - + // this should be done prior to calling this function. + void ClearHoldingPlane(TowerPlaneRec* t); + + // Find a pointer to plane of callsign ID within the internal data structures + TowerPlaneRec* FindPlane(const string& ID); + + // Remove and delete all instances of a plane with a given ID + void RemovePlane(const string& ID); + + // Figure out if a given position lies on the active runway + // Might have to change when we consider more than one active rwy. + bool OnActiveRunway(const Point3D& pt); + + // Figure out if a given position lies on a runway or not + bool OnAnyRunway(const Point3D& pt, bool onGround); + + // Calculate the eta of a plane to the threshold. + // For ground traffic this is the fastest they can get there. + // For air traffic this is the middle approximation. + void CalcETA(TowerPlaneRec* tpr, bool printout = false); + + // Iterate through all the lists and call CalcETA for all the planes. + void doThresholdETACalc(); + + // Order the list of traffic as per expected threshold use and flag any conflicts + bool doThresholdUseOrder(); + + // Calculate the crow-flys distance of a plane to the threshold in meters + double CalcDistOutM(TowerPlaneRec* tpr); + + // Calculate the crow-flys distance of a plane to the threshold in miles + double CalcDistOutMiles(TowerPlaneRec* tpr); + + /* + void doCommunication(); + */ + + void IssueLandingClearance(TowerPlaneRec* tpr); + void IssueGoAround(TowerPlaneRec* tpr); + void IssueDepartureClearance(TowerPlaneRec* tpr); + + unsigned int update_count; // Convienince counter for speading computational load over several updates + unsigned int update_count_max; // ditto. + + double timeSinceLastDeparture; // Time in seconds since last departure from active rwy. + bool departed; // set true when the above needs incrementing with time, false when it doesn't. + + // environment - need to make sure we're getting the surface winds and not winds aloft. + SGPropertyNode_ptr wind_from_hdg; //degrees + SGPropertyNode_ptr wind_speed_knots; //knots + + double aptElev; // Airport elevation + string activeRwy; // Active runway number - For now we'll disregard multiple / alternate runway operation. + RunwayDetails rwy; // Assumed to be the active one for now. + bool rwyOccupied; // Active runway occupied flag. For now we'll disregard land-and-hold-short operations. + FGATCAlignedProjection ortho; // Orthogonal mapping of the local area with the active runway threshold at the origin + FGATCAlignedProjection ortho_temp; // Ortho for any runway (needed to get plane position in airport) + + // Figure out which runways are active. + // For now we'll just be simple and do one active runway - eventually this will get much more complex + // This is a private function - public interface to the results of this is through GetActiveRunway + void DoRwyDetails(); + + // Need a data structure to hold details of the various active planes + // or possibly another data structure with the positions of the inactive planes. + // Need a data structure to hold outstanding communications from aircraft. + + // Linked-list of planes on approach to active rwy ordered with nearest first (timewise). + // Includes planes that have landed but not yet vacated runway. + // Somewhat analagous to the paper strips used (used to be used?) in real life. + // Doesn't include planes in circuit until they turn onto base/final? + // TODO - may need to alter this for operation to more than one active rwy. + tower_plane_rec_list_type appList; + tower_plane_rec_list_iterator appListItr; + + // What should we do with planes approaching the airport to join the circuit somewhere + // but not on straight-in though? - put them in here for now. + tower_plane_rec_list_type circuitAppList; + tower_plane_rec_list_iterator circuitAppListItr; + + // List of departed planes (planes doing circuits go into circuitList not depList after departure) + tower_plane_rec_list_type depList; + tower_plane_rec_list_iterator depListItr; + + // List of planes in the circuit (ordered by nearest to landing first) + tower_plane_rec_list_type circuitList; + tower_plane_rec_list_iterator circuitListItr; + + // List of planes waiting to depart + tower_plane_rec_list_type holdList; + tower_plane_rec_list_iterator holdListItr; + + // List of planes on rwy + tower_plane_rec_list_type rwyList; + tower_plane_rec_list_iterator rwyListItr; + + // List of all planes due to use a given rwy arranged in projected order of rwy use + tower_plane_rec_list_type trafficList; // TODO - needs to be expandable to more than one rwy + tower_plane_rec_list_iterator trafficListItr; + + // List of planes that have vacated the runway inbound but not yet handed off to ground + tower_plane_rec_list_type vacatedList; + tower_plane_rec_list_iterator vacatedListItr; + + // Returns true if successful + bool RemoveFromTrafficList(const string& id); + bool RemoveFromAppList(const string& id); + bool RemoveFromRwyList(const string& id); + + // Return the ETA of plane no. list_pos (1-based) in the traffic list. + // i.e. list_pos = 1 implies next to use runway. + double GetTrafficETA(unsigned int list_pos, bool printout = false); + + // Add a tower plane rec with ETA to the traffic list in the correct position ETA-wise. + // Returns true if this could cause a threshold ETA conflict with other traffic, false otherwise. + bool AddToTrafficList(TowerPlaneRec* t, bool holding = false); + + bool AddToCircuitList(TowerPlaneRec* t); + + // Add to vacated list only if not already present + void AddToVacatedList(TowerPlaneRec* t); + + void AddToHoldingList(TowerPlaneRec* t); + + // Ground can be separate or handled by tower in real life. + // In the program we will always use a separate FGGround class, but we need to know + // whether it is supposed to be separate or not to give the correct instructions. + bool separateGround; // true if ground control is separate + FGGround* ground; // The ground control associated with this airport. + + bool _departureControlled; // true if we need to hand off departing traffic to departure control + //FGDeparture* _departure; // The relevant departure control (once we've actually written it!) + + // for failure modeling + string trans_ident; // transmitted ident + bool tower_failed; // tower failed? + + // Pointers to current users position and orientation + SGPropertyNode_ptr user_lon_node; + SGPropertyNode_ptr user_lat_node; + SGPropertyNode_ptr user_elev_node; + SGPropertyNode_ptr user_hdg_node; + + // Details of the general traffic flow etc in the circuit + double crosswind_leg_pos; // Distance from threshold crosswind leg is being turned to in meters (actual operation - *not* ideal circuit) + double downwind_leg_pos; // Actual offset distance in meters from the runway that planes are flying the downwind leg + // Currently not sure whether the above should be always +ve or just take the natural orthopos sign (+ve for RH circuit, -ve for LH). + double base_leg_pos; // Actual offset distance from the threshold (-ve) that planes are turning to base leg. + + double nominal_crosswind_leg_pos; + double nominal_downwind_leg_pos; + double nominal_base_leg_pos; + + friend istream& operator>> ( istream&, FGTower& ); +}; + +#endif //_FG_TOWER_HXX diff --git a/src/ATCDCL/transmission.cxx b/src/ATCDCL/transmission.cxx new file mode 100644 index 000000000..0a6ea4b76 --- /dev/null +++ b/src/ATCDCL/transmission.cxx @@ -0,0 +1,100 @@ +// FGTransmission - a class to provide transmission control at larger airports. +// +// Written by Alexander Kappes, started March 2002. +// Based on ground.cxx by David Luff, started March 2002. +// +// Copyright (C) 2002 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#include "transmission.hxx" + +#include <simgear/misc/sg_path.hxx> + + +//Constructor +FGTransmission::FGTransmission(){ +} + +//Destructor +FGTransmission::~FGTransmission(){ +} + +void FGTransmission::Init() { +} + +// ============================================================================ +// extract parameters from transmission +// ============================================================================ +TransPar FGTransmission::Parse() { + TransPar tpar; + string tokens[20]; + int msglen,toklen; + //char dum; + int i,j,k; + const char *capl = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + msglen = strlen( TransText.c_str() ); + + int tkn = 0; + for ( i=0; i < msglen; ++i ) { + if ( TransText.c_str()[i] != ' ' ) { + if ( TransText.c_str()[i] != ',' ) tokens[tkn] += TransText.c_str()[i]; + } else if ( !tokens[tkn].empty() ) { + if ( tkn <= 20 ) { + tkn += 1; + } else { + cout << "Too many tokens" << endl; + } + } + } + + for ( i=0; i<20; ++i) { + + if ( tokens[i] == "request" ) { + tpar.request = true; + } else if ( tokens[i] == "approach" ) { + tpar.station = "approach"; + tpar.airport = tokens[i-1]; + } else if ( tokens[i] == "landing" ) { + tpar.intention = "landing"; + for ( j=i+1; j<=i+2; ++j ) { + if ( !tokens[j].empty() ) { + toklen = strlen( tokens[j].c_str() ); + bool aid = true; + for ( k=0; k<toklen; ++k ) + if ( ! strpbrk( &tokens[j].c_str()[k], capl )) { + aid = false; + break; + } + if ( aid ) tpar.intid = tokens[j]; + } + } + } else if ( tokens[i] == "Player" ) { + tpar.callsign = tokens[i]; + } + } + + //cout << tpar.airport << endl; + //cout << tpar.request << endl; + //cout << tpar.intention << endl; + //cout << tpar.intid << endl; + + return tpar; +} diff --git a/src/ATCDCL/transmission.hxx b/src/ATCDCL/transmission.hxx new file mode 100644 index 000000000..c1881b3c6 --- /dev/null +++ b/src/ATCDCL/transmission.hxx @@ -0,0 +1,169 @@ +// transmission.hxx -- Transmission class +// +// Written by Alexander Kappes, started March 2002. +// Based on nav.hxx by Curtis Olson, started April 2000. +// +// Copyright (C) 2001 David C. Luff - david.luff@nottingham.ac.uk +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#ifndef _FG_TRANSMISSION_HXX +#define _FG_TRANSMISSION_HXX + +#include <stdio.h> + +#include <simgear/compiler.h> +#include <simgear/math/sg_geodesy.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/magvar/magvar.hxx> +#include <simgear/timing/sg_time.hxx> +#include <simgear/bucket/newbucket.hxx> + +#include <Main/fg_props.hxx> + +#ifdef SG_HAVE_STD_INCLUDES +# include <istream> +#include <iomanip> +#elif defined( SG_HAVE_NATIVE_SGI_COMPILERS ) +# include <iostream.h> +#elif defined( __BORLANDC__ ) +# include <iostream> +#else +# include <istream.h> +#include <iomanip.h> +#endif + +#include "ATC.hxx" + +#if ! defined( SG_HAVE_NATIVE_SGI_COMPILERS ) +SG_USING_STD(istream); +#endif + +SG_USING_STD(string); + +struct TransCode { + int c1; + int c2; + int c3; +}; + +// TransPar - a representation of the logic of a parsed speech transmission +struct TransPar { + string station; + string callsign; + string airport; + string intention; // landing, crossing + string intid; // (airport) ID for intention + bool request; // is the transmission a request or an answer? + int tdir; // turning direction: 1=left, 2=right + double heading; + int VDir; // vertical direction: 1=descent, 2=maintain, 3=climb + double alt; + double miles; + string runway; + double freq; + double time; +}; + +// FGTransmission - a class to encapsulate a speech transmission +class FGTransmission { + + //int StationType; // Type of ATC station: 1 Approach + atc_type StationType; + TransCode Code; // DCL - no idea what this is. + string TransText; // The text of the spoken transmission + string MenuText; // An abbreviated version of the text for the menu entry + +public: + + FGTransmission(void); + ~FGTransmission(void); + + void Init(); + + inline atc_type get_station() const { return StationType; } + inline const TransCode& get_code() { return Code; } + inline const string& get_transtext() { return TransText; } + inline const string& get_menutext() { return MenuText; } + + // Return the parsed logic of the transmission + TransPar Parse(); + +private: + + friend istream& operator>> ( istream&, FGTransmission& ); + +}; + + +inline istream& +operator >> ( istream& in, FGTransmission& a ) { + char ch; + int tmp; + + static bool first_time = true; + static double julian_date = 0; + static const double MJD0 = 2415020.0; + if ( first_time ) { + julian_date = sgTimeCurrentMJD(0, 0) + MJD0; + first_time = false; + } + // Ugly hack alert - eventually we'll use xml format for the transmissions file + in >> tmp; + if(tmp == 1) { + a.StationType = APPROACH; + } else { + a.StationType = INVALID; + } + in >> a.Code.c1; + in >> a.Code.c2; + in >> a.Code.c3; + a.TransText = ""; + in >> ch; + if ( ch != '"' ) a.TransText += ch; + while(1) { + //in >> noskipws + in.unsetf(ios::skipws); + in >> ch; + if ( ch != '"' ) a.TransText += ch; + if((ch == '"') || (ch == 0x0A)) { + break; + } // we shouldn't need the 0x0A but it makes a nice safely in case someone leaves off the " + } + in.setf(ios::skipws); + + a.MenuText = ""; + in >> ch; + if ( ch != '"' ) a.MenuText += ch; + while(1) { + //in >> noskipws + in.unsetf(ios::skipws); + in >> ch; + if ( ch != '"' ) a.MenuText += ch; + if((ch == '"') || (ch == 0x0A)) { + break; + } // we shouldn't need the 0x0A but it makes a nice safely in case someone leaves off the " + } + in.setf(ios::skipws); + + //cout << "Code = " << a.Code << " Transmission text = " << a.TransText + // << " Menu text = " << a.MenuText << endl; + + return in >> skipeol; +} + + +#endif // _FG_TRANSMISSION_HXX diff --git a/src/ATCDCL/transmissionlist.cxx b/src/ATCDCL/transmissionlist.cxx new file mode 100644 index 000000000..cb24e50ca --- /dev/null +++ b/src/ATCDCL/transmissionlist.cxx @@ -0,0 +1,263 @@ +// transmissionlist.cxx -- transmission management class +// +// Written by Alexander Kappes, started March 2002. +// Based on navlist.cxx by Curtis Olson, started April 2000. +// +// Copyright (C) 2000 Curtis L. Olson - http://www.flightgear.org/~curt +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// $Id$ + + +#ifdef HAVE_CONFIG_H +# include <config.h> +#endif + +#ifdef HAVE_STRINGS_H +# include <strings.h> // bcopy() +#else +# include <string.h> // MSVC doesn't have strings.h +#endif + + +#include <simgear/debug/logstream.hxx> +#include <simgear/misc/sgstream.hxx> +#include <simgear/math/sg_geodesy.hxx> + +#include "transmissionlist.hxx" + +#include <GUI/gui.h> + + +FGTransmissionList *current_transmissionlist; + + +FGTransmissionList::FGTransmissionList( void ) { +} + + +FGTransmissionList::~FGTransmissionList( void ) { +} + + +// load default.transmissions +bool FGTransmissionList::init( const SGPath& path ) { + FGTransmission a; + + transmissionlist_station.erase( transmissionlist_station.begin(), transmissionlist_station.end() ); + + sg_gzifstream in( path.str() ); + if ( !in.is_open() ) { + SG_LOG( SG_GENERAL, SG_ALERT, "Cannot open file: " << path.str() ); + exit(-1); + } + + // read in each line of the file + + // in >> skipeol; + // in >> skipcomment; + +#ifdef __MWERKS__ + + char c = 0; + while ( in.get(c) && c != '\0' ) { + in.putback(c); + in >> a; + if ( a.get_type() != '[' ) { + transmissionlist_code[a.get_station()].push_back(a); + } + in >> skipcomment; + } + +#else + + double min = 100000; + double max = 0; + + while ( ! in.eof() ) { + in >> a; + transmissionlist_station[a.get_station()].push_back(a); + + in >> skipcomment; + + if ( a.get_station() < min ) { + min = a.get_station(); + } + if ( a.get_station() > max ) { + max = a.get_station(); + } + + /* + cout << a.get_station() << " " << a.get_code().c1 << " " << a.get_code().c2 << " " + << a.get_code().c3 << " " << a.get_transtext() + << " " << a.get_menutext() << endl; + */ + } + +#endif + + // init ATC menu + fgSetBool("/sim/atc/menu",false); + + return true; +} + +// query the database for the specified station type; +// for station see FlightGear/ATC/default.transmissions +bool FGTransmissionList::query_station( const atc_type &station, FGTransmission *t, + int max_trans, int &num_trans ) +{ + transmission_list_type tmissions = transmissionlist_station[station]; + transmission_list_iterator current = tmissions.begin(); + transmission_list_iterator last = tmissions.end(); + + for ( ; current != last ; ++current ) { + if (num_trans < max_trans) { + t[num_trans] = *current; + num_trans += 1; + } + else { + cout << "Transmissionlist error: Too many transmissions" << endl; + } + } + + if ( num_trans != 0 ) return true; + else { + cout << "No transmission with station " << station << "found." << endl; + string empty; + return false; + } +} + +string FGTransmissionList::gen_text(const atc_type &station, const TransCode code, + const TransPar &tpars, const bool ttext ) +{ + const int cmax = 300; + string message; + char tag[4]; + char crej = '@'; + char mes[cmax]; + char dum[cmax]; + //char buf[10]; + char *pos; + int len; + FGTransmission t; + + // if (current_transmissionlist->query_station( station, &t ) ) { + transmission_list_type tmissions = transmissionlist_station[station]; + transmission_list_iterator current = tmissions.begin(); + transmission_list_iterator last = tmissions.end(); + + for ( ; current != last ; ++current ) { + if ( current->get_code().c1 == code.c1 && + current->get_code().c2 == code.c2 && + current->get_code().c3 == code.c3 ) { + + if ( ttext ) message = current->get_transtext(); + else message = current->get_menutext(); + strcpy( &mes[0], message.c_str() ); + + // Replace all the '@' parameters with the actual text. + int check = 0; // If mes gets overflowed the while loop can go infinite + while ( strchr(&mes[0], crej) != NULL ) { // ie. loop until no more occurances of crej ('@') found + pos = strchr( &mes[0], crej ); + memmove(&tag[0], pos, 3); + tag[3] = '\0'; + int i; + len = 0; + for ( i=0; i<cmax; i++ ) { + if ( mes[i] == crej ) { + len = i; + break; + } + } + strncpy( &dum[0], &mes[0], len ); + dum[len] = '\0'; + + if ( strcmp ( tag, "@ST" ) == 0 ) + strcat( &dum[0], tpars.station.c_str() ); + else if ( strcmp ( tag, "@AP" ) == 0 ) + strcat( &dum[0], tpars.airport.c_str() ); + else if ( strcmp ( tag, "@CS" ) == 0 ) + strcat( &dum[0], tpars.callsign.c_str() ); + else if ( strcmp ( tag, "@TD" ) == 0 ) { + if ( tpars.tdir == 1 ) { + char buf[] = "left"; + strcat( &dum[0], &buf[0] ); + } + else { + char buf[] = "right"; + strcat( &dum[0], &buf[0] ); + } + } + else if ( strcmp ( tag, "@HE" ) == 0 ) { + char buf[10]; + sprintf( buf, "%i", (int)(tpars.heading) ); + strcat( &dum[0], &buf[0] ); + } + else if ( strcmp ( tag, "@VD" ) == 0 ) { + if ( tpars.VDir == 1 ) { + char buf[] = "Descend and maintain"; + strcat( &dum[0], &buf[0] ); + } + else if ( tpars.VDir == 2 ) { + char buf[] = "Maintain"; + strcat( &dum[0], &buf[0] ); + } + else if ( tpars.VDir == 3 ) { + char buf[] = "Climb and maintain"; + strcat( &dum[0], &buf[0] ); + } + } + else if ( strcmp ( tag, "@AL" ) == 0 ) { + char buf[10]; + sprintf( buf, "%i", (int)(tpars.alt) ); + strcat( &dum[0], &buf[0] ); + } + else if ( strcmp ( tag, "@MI" ) == 0 ) { + char buf[10]; + sprintf( buf, "%3.1f", tpars.miles ); + strcat( &dum[0], &buf[0] ); + } + else if ( strcmp ( tag, "@FR" ) == 0 ) { + char buf[10]; + sprintf( buf, "%6.2f", tpars.freq ); + strcat( &dum[0], &buf[0] ); + } + else if ( strcmp ( tag, "@RW" ) == 0 ) + strcat( &dum[0], tpars.runway.c_str() ); + else { + cout << "Tag " << tag << " not found" << endl; + break; + } + strcat( &dum[0], &mes[len+3] ); + strcpy( &mes[0], &dum[0] ); + + ++check; + if(check > 10) { + SG_LOG(SG_ATC, SG_WARN, "WARNING: Possibly endless loop terminated in FGTransmissionlist::gen_text(...)"); + break; + } + } + + //cout << mes << endl; + break; + } + } + return mes[0] ? mes : "No transmission found"; +} + + diff --git a/src/ATCDCL/transmissionlist.hxx b/src/ATCDCL/transmissionlist.hxx new file mode 100644 index 000000000..ae6be8a8b --- /dev/null +++ b/src/ATCDCL/transmissionlist.hxx @@ -0,0 +1,82 @@ +// transmissionlist.hxx -- transmission management class +// +// Written by Alexander Kappes, started March 2002. +// Based on navlist.hxx by Curtis Olson, started April 2000. +// +// Copyright (C) 2000 Curtis L. Olson - http://www.flightgear.org/~curt +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// + + +#ifndef _FG_TRANSMISSIONLIST_HXX +#define _FG_TRANSMISSIONLIST_HXX + + +#include <simgear/compiler.h> +#include <simgear/misc/sg_path.hxx> + +#include <map> +#include <vector> + +#include "ATC.hxx" +#include "transmission.hxx" + +SG_USING_STD(map); +SG_USING_STD(vector); + +class FGTransmissionList { + + // convenience types + typedef vector < FGTransmission > transmission_list_type; + typedef transmission_list_type::iterator transmission_list_iterator; + typedef transmission_list_type::const_iterator transmission_list_const_iterator; + + // Map of transmission lists by station type + // typedef map < int, transmission_list_type, less<int> > transmission_map_type; + typedef map < atc_type, transmission_list_type > transmission_map_type; + typedef transmission_map_type::iterator transmission_map_iterator; + typedef transmission_map_type::const_iterator transmission_map_const_iterator; + + transmission_map_type transmissionlist_station; + +public: + + FGTransmissionList(); + ~FGTransmissionList(); + + // load the transmission data and build the map + bool init( const SGPath& path ); + + // query the database for the specified code, + bool query_station( const atc_type &station, FGTransmission *a, int max_trans, int &num_trans ); + + // generate the transmission text given the code of the message + // and the parameters + // Set ttext = true to generate the spoken transmission text, + // or false to generate the abridged menu entry text. + string gen_text(const atc_type &station, const TransCode code, + const TransPar &tpars, const bool ttext); + +}; + + +void mkATCMenuInit (void); +void mkATCMenu (void); + +extern FGTransmissionList *current_transmissionlist; + + +#endif // _FG_TRANSMISSIONLIST_HXX