From 0be2e03d7ebb469ec7731e4bc8d9145f3d18791c Mon Sep 17 00:00:00 2001 From: jmt Date: Thu, 25 Mar 2010 16:40:52 +0000 Subject: [PATCH 01/13] GPS: fix bugs where GPS overwrites NAV1 course, including --nav1 command line --- src/Instrumentation/gps.cxx | 117 ++++++++----------------------- src/Instrumentation/gps.hxx | 32 ++++----- src/Instrumentation/navradio.cxx | 2 +- 3 files changed, 45 insertions(+), 106 deletions(-) diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index 2f7455b83..16714a9c9 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -188,75 +188,27 @@ GPS::Config::Config() : _turnRate(3.0), // degrees-per-second, so 180 degree turn takes 60 seconds _overflightArmDistance(1.0), _waypointAlertTime(30.0), - _tuneRadio1ToRefVor(false), _minRunwayLengthFt(0.0), _requireHardSurface(true), _cdiMaxDeflectionNm(3.0), // linear mode, 3nm at the peg - _driveAutopilot(true) + _driveAutopilot(true), + _courseSelectable(false) { _enableTurnAnticipation = false; - _extCourseSource = fgGetNode("/instrumentation/nav[0]/radials/selected-deg", true); } void GPS::Config::bind(GPS* aOwner, SGPropertyNode* aCfg) { aOwner->tie(aCfg, "turn-rate-deg-sec", SGRawValuePointer(&_turnRate)); - aOwner->tie(aCfg, "turn-anticipation", SGRawValuePointer(&_enableTurnAnticipation)); aOwner->tie(aCfg, "wpt-alert-time", SGRawValuePointer(&_waypointAlertTime)); - aOwner->tie(aCfg, "tune-nav-radio-to-ref-vor", SGRawValuePointer(&_tuneRadio1ToRefVor)); aOwner->tie(aCfg, "min-runway-length-ft", SGRawValuePointer(&_minRunwayLengthFt)); aOwner->tie(aCfg, "hard-surface-runways-only", SGRawValuePointer(&_requireHardSurface)); - - aOwner->tie(aCfg, "course-source", SGRawValueMethods - (*this, &GPS::Config::getCourseSource, &GPS::Config::setCourseSource)); - aOwner->tie(aCfg, "cdi-max-deflection-nm", SGRawValuePointer(&_cdiMaxDeflectionNm)); aOwner->tie(aCfg, "drive-autopilot", SGRawValuePointer(&_driveAutopilot)); + aOwner->tie(aCfg, "course-selectable", SGRawValuePointer(&_courseSelectable)); } -const char* -GPS::Config::getCourseSource() const -{ - if (!_extCourseSource) { - return ""; - } - - return _extCourseSource->getPath(true); -} - -void -GPS::Config::setCourseSource(const char* aPath) -{ - SGPropertyNode* nd = fgGetNode(aPath, false); - if (!nd) { - SG_LOG(SG_INSTR, SG_WARN, "couldn't find course source at:" << aPath); - _extCourseSource = NULL; - } - - _extCourseSource = nd; -} - -double -GPS::Config::getExternalCourse() const -{ - if (!_extCourseSource) { - return 0.0; - } - - return _extCourseSource->getDoubleValue(); -} - -void -GPS::Config::setExternalCourse(double aCourseDeg) -{ - if (!_extCourseSource) { - return; - } - - _extCourseSource->setDoubleValue(aCourseDeg); -} - //////////////////////////////////////////////////////////////////////////// GPS::GPS ( SGPropertyNode *node) : @@ -304,7 +256,7 @@ GPS::init () // for compatability, alias selected course down to wp/wp[1]/desired-course-deg SGPropertyNode* wp1Crs = wp1_node->getChild("desired-course-deg", 0, true); - wp1Crs->alias(_gpsNode->getChild("selected-course-deg")); + wp1Crs->alias(_gpsNode->getChild("desired-course-deg", 0, true)); // _true_wp1_bearing_error_node = // wp1_node->getChild("true-bearing-error-deg", 0, true); @@ -371,9 +323,12 @@ GPS::bind() _config.bind(this, _gpsNode->getChild("config", 0, true)); // basic GPS outputs tie(_gpsNode, "selected-course-deg", SGRawValueMethods - (*this, &GPS::getSelectedCourse, NULL)); - + (*this, &GPS::getSelectedCourse, &GPS::setSelectedCourse)); + tie(_gpsNode, "desired-course-deg", SGRawValueMethods + (*this, &GPS::getDesiredCourse, NULL)); + _desiredCourseNode = _gpsNode->getChild("desired-course-deg"); + tieSGGeodReadOnly(_gpsNode, _indicated_pos, "indicated-longitude-deg", "indicated-latitude-deg", "indicated-altitude-ft"); @@ -539,9 +494,7 @@ GPS::update (double delta_time_sec) updateBasicData(delta_time_sec); if (_dataValid) { - if (_mode == "obs") { - _selectedCourse = _config.getExternalCourse(); - } else { + if (_mode != "obs") { updateTurn(); } @@ -555,9 +508,7 @@ GPS::update (double delta_time_sec) if (_dataValid && (_mode == "init")) { // allow a realistic delay in the future, here SG_LOG(SG_INSTR, SG_INFO, "GPS initialisation complete"); - - _selectedCourse = _config.getExternalCourse(); - + if (_route_active_node->getBoolValue()) { // GPS init with active route SG_LOG(SG_INSTR, SG_INFO, "GPS init with active route"); @@ -655,7 +606,6 @@ void GPS::updateReferenceNavaid(double dt) FGNavRecord* vor = (FGNavRecord*) nav.ptr(); _ref_navaid_frequency_node->setDoubleValue(vor->get_freq() / 100.0); _listener->setGuard(false); - tuneNavRadios(); } else { // SG_LOG(SG_INSTR, SG_ALERT, "matched existing"); } @@ -694,29 +644,12 @@ void GPS::referenceNavaidSet(const std::string& aNavaid) _ref_navaid_name_node->setStringValue(_ref_navaid->name().c_str()); FGNavRecord* vor = (FGNavRecord*) _ref_navaid.ptr(); _ref_navaid_frequency_node->setDoubleValue(vor->get_freq() / 100.0); - tuneNavRadios(); } else { _ref_navaid_set = false; _ref_navaid_elapsed = 9999.0; // update next tick } } -void GPS::tuneNavRadios() -{ - if (!_ref_navaid || !_config.tuneNavRadioToRefVor()) { - return; - } - - SGPropertyNode_ptr navRadio1 = fgGetNode("/instrumentation/nav", false); - if (!navRadio1) { - return; - } - - FGNavRecord* vor = (FGNavRecord*) _ref_navaid.ptr(); - SGPropertyNode_ptr freqs = navRadio1->getChild("frequencies"); - freqs->setDoubleValue("selected-mhz", vor->get_freq() / 100.0); -} - void GPS::routeActivated() { if (_route_active_node->getBoolValue()) { @@ -763,7 +696,8 @@ void GPS::routeManagerSequenced() _wp1Name = wp1.get_name(); _wp1_position = wp1.get_target(); - _selectedCourse = getLegMagCourse(); + _desiredCourse = getLegMagCourse(); + _desiredCourseNode->fireValueChanged(); wp1Changed(); } @@ -917,7 +851,7 @@ void GPS::computeTurnData() return; } - _turnStartBearing = _selectedCourse; + _turnStartBearing = _desiredCourse; // compute next leg course SGWayPoint wp1(_routeMgr->get_waypoint(curIndex)), wp2(_routeMgr->get_waypoint(curIndex + 1)); @@ -1013,9 +947,6 @@ void GPS::driveAutopilot() void GPS::wp1Changed() { - // update external HSI/CDI/NavDisplay/PFD/etc - _config.setExternalCourse(getLegMagCourse()); - if (!_config.driveAutopilot()) { return; } @@ -1029,6 +960,19 @@ void GPS::wp1Changed() ///////////////////////////////////////////////////////////////////////////// // property getter/setters +void GPS::setSelectedCourse(double crs) +{ + if (_selectedCourse == crs) { + return; + } + + _selectedCourse = crs; + if ((_mode == "obs") || _config.courseSelectable()) { + _desiredCourse = _selectedCourse; + _desiredCourseNode->fireValueChanged(); + } +} + double GPS::getLegDistance() const { if (!_dataValid || (_mode == "obs")) { @@ -1197,7 +1141,7 @@ double GPS::getWP1CourseDeviation() const return 0.0; } - double dev = getWP1MagBearing() - _selectedCourse; + double dev = getWP1MagBearing() - _desiredCourse; SG_NORMALIZE_RANGE(dev, -180.0, 180.0); if (fabs(dev) > 90.0) { @@ -1227,7 +1171,7 @@ bool GPS::getWP1ToFlag() const return false; } - double dev = getWP1MagBearing() - _selectedCourse; + double dev = getWP1MagBearing() - _desiredCourse; SG_NORMALIZE_RANGE(dev, -180.0, 180.0); return (fabs(dev) < 90.0); @@ -1368,7 +1312,8 @@ void GPS::directTo() _wp1_position = _scratchPos; _mode = "dto"; - _selectedCourse = getLegMagCourse(); + _desiredCourse = getLegMagCourse(); + _desiredCourseNode->fireValueChanged(); clearScratch(); wp1Changed(); diff --git a/src/Instrumentation/gps.hxx b/src/Instrumentation/gps.hxx index d20c74e48..24c8f50fb 100644 --- a/src/Instrumentation/gps.hxx +++ b/src/Instrumentation/gps.hxx @@ -123,19 +123,12 @@ private: double waypointAlertTime() const { return _waypointAlertTime; } - bool tuneNavRadioToRefVor() const - { return _tuneRadio1ToRefVor; } - bool requireHardSurface() const { return _requireHardSurface; } double minRunwayLengthFt() const { return _minRunwayLengthFt; } - double getExternalCourse() const; - - void setExternalCourse(double aCourseDeg); - bool cdiDeflectionIsAngular() const { return (_cdiMaxDeflectionNm <= 0.0); } @@ -147,6 +140,9 @@ private: bool driveAutopilot() const { return _driveAutopilot; } + + bool courseSelectable() const + { return _courseSelectable; } private: bool _enableTurnAnticipation; @@ -160,25 +156,19 @@ private: // (in seconds) double _waypointAlertTime; - // should GPS automatically tune NAV1 to the reference VOR? - bool _tuneRadio1ToRefVor; - // minimum runway length to require when filtering double _minRunwayLengthFt; // should we require a hard-surfaced runway when filtering? bool _requireHardSurface; - // helpers to tie course-source property - const char* getCourseSource() const; - void setCourseSource(const char* aPropPath); - - // property to retrieve the external course from - SGPropertyNode_ptr _extCourseSource; - double _cdiMaxDeflectionNm; + // should we drive the autopilot directly or not? bool _driveAutopilot; + + // is selected-course-deg read to set desired-course or not? + bool _courseSelectable; }; class SearchFilter : public FGPositioned::Filter @@ -201,7 +191,6 @@ private: void updateTrackingBug(); void updateReferenceNavaid(double dt); void referenceNavaidSet(const std::string& aNavaid); - void tuneNavRadios(); void updateRouteData(); void driveAutopilot(); @@ -276,6 +265,9 @@ private: bool getScratchHasNext() const { return _searchHasNext; } double getSelectedCourse() const { return _selectedCourse; } + void setSelectedCourse(double crs); + double getDesiredCourse() const { return _desiredCourse; } + double getCDIDeflection() const; double getLegDistance() const; @@ -355,8 +347,10 @@ private: SGPropertyNode_ptr _routeETE; SGPropertyNode_ptr _routeEditedSignal; SGPropertyNode_ptr _routeFinishedSignal; - + SGPropertyNode_ptr _desiredCourseNode; + double _selectedCourse; + double _desiredCourse; bool _dataValid; SGGeod _last_pos; diff --git a/src/Instrumentation/navradio.cxx b/src/Instrumentation/navradio.cxx index 496e09518..b8834a3c6 100644 --- a/src/Instrumentation/navradio.cxx +++ b/src/Instrumentation/navradio.cxx @@ -224,7 +224,7 @@ FGNavRadio::init () gps_to_flag_node = fgGetNode("/instrumentation/gps/to-flag", true); gps_from_flag_node = fgGetNode("/instrumentation/gps/from-flag", true); gps_has_gs_node = fgGetNode("/instrumentation/gps/has-gs", true); - gps_course_node = fgGetNode("/instrumentation/gps/selected-course-deg", true); + gps_course_node = fgGetNode("/instrumentation/gps/desired-course-deg", true); gps_xtrack_error_nm_node = fgGetNode("/instrumentation/gps/wp/wp[1]/course-error-nm", true); _magvarNode = fgGetNode("/environment/magnetic-variation-deg", true); From 4265b2e2419bd9da2d6e83851c9bd0ab3e41fdb0 Mon Sep 17 00:00:00 2001 From: jmt Date: Thu, 25 Mar 2010 16:41:50 +0000 Subject: [PATCH 02/13] GPS: make slaved-to-gps read course from the GPS *when active*, via a listener. --- src/Instrumentation/navradio.cxx | 27 +++++++++++++++++++++++++++ src/Instrumentation/navradio.hxx | 5 ++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Instrumentation/navradio.cxx b/src/Instrumentation/navradio.cxx index b8834a3c6..4bfe15d48 100644 --- a/src/Instrumentation/navradio.cxx +++ b/src/Instrumentation/navradio.cxx @@ -129,6 +129,9 @@ FGNavRadio::FGNavRadio(SGPropertyNode *node) : // Destructor FGNavRadio::~FGNavRadio() { + gps_course_node->removeChangeListener(this); + nav_slaved_to_gps_node->removeChangeListener(this); + delete term_tbl; delete low_tbl; delete high_tbl; @@ -220,11 +223,15 @@ FGNavRadio::init () // gps slaving support nav_slaved_to_gps_node = node->getChild("slaved-to-gps", 0, true); + nav_slaved_to_gps_node->addChangeListener(this); + gps_cdi_deflection_node = fgGetNode("/instrumentation/gps/cdi-deflection", true); gps_to_flag_node = fgGetNode("/instrumentation/gps/to-flag", true); gps_from_flag_node = fgGetNode("/instrumentation/gps/from-flag", true); gps_has_gs_node = fgGetNode("/instrumentation/gps/has-gs", true); gps_course_node = fgGetNode("/instrumentation/gps/desired-course-deg", true); + gps_course_node->addChangeListener(this); + gps_xtrack_error_nm_node = fgGetNode("/instrumentation/gps/wp/wp[1]/course-error-nm", true); _magvarNode = fgGetNode("/environment/magnetic-variation-deg", true); @@ -623,6 +630,26 @@ void FGNavRadio::updateDME(const SGVec3d& aircraft) _dmeInRange = (dme_distance < _dme->get_range() * SG_NM_TO_METER); } +void FGNavRadio::valueChanged (SGPropertyNode* prop) +{ + if (prop == gps_course_node) { + if (!nav_slaved_to_gps_node->getBoolValue()) { + return; + } + + // GPS desired course has changed, sync up our selected-course + double v = prop->getDoubleValue(); + if (v != sel_radial_node->getDoubleValue()) { + sel_radial_node->setDoubleValue(v); + } + } else if (prop == nav_slaved_to_gps_node) { + if (prop->getBoolValue()) { + // slaved-to-GPS activated, sync up selected course + sel_radial_node->setDoubleValue(gps_course_node->getDoubleValue()); + } + } +} + void FGNavRadio::updateGPSSlaved() { has_gs_node->setBoolValue(gps_has_gs_node->getBoolValue()); diff --git a/src/Instrumentation/navradio.hxx b/src/Instrumentation/navradio.hxx index 41d662ff0..5cc368f1b 100644 --- a/src/Instrumentation/navradio.hxx +++ b/src/Instrumentation/navradio.hxx @@ -39,7 +39,7 @@ class SGSampleGroup; class FGNavRecord; typedef SGSharedPtr FGNavRecordPtr; -class FGNavRadio : public SGSubsystem +class FGNavRadio : public SGSubsystem, public SGPropertyChangeListener { FGMorse morse; @@ -212,6 +212,9 @@ class FGNavRadio : public SGSubsystem _tiedNodes.push_back(nd); nd->tie(aRawValue); } + + // implement SGPropertyChangeListener + virtual void valueChanged (SGPropertyNode * prop); public: FGNavRadio(SGPropertyNode *node); From 40e383451bf71e05acb2a5c757eb9c32bbcac624 Mon Sep 17 00:00:00 2001 From: jmt Date: Sat, 27 Mar 2010 11:37:06 +0000 Subject: [PATCH 03/13] GPS: add NS and EW velocity computation, which some real-world devices display. --- src/Instrumentation/gps.cxx | 26 +++++++++++++++++++++++++- src/Instrumentation/gps.hxx | 4 ++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index 16714a9c9..4eee22a69 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -249,6 +249,8 @@ GPS::init () _trip_odometer_node = _gpsNode->getChild("trip-odometer", 0, true); _true_bug_error_node = _gpsNode->getChild("true-bug-error-deg", 0, true); _magnetic_bug_error_node = _gpsNode->getChild("magnetic-bug-error-deg", 0, true); + _eastWestVelocity = _gpsNode->getChild("ew-velocity-msec", 0, true); + _northSouthVelocity = _gpsNode->getChild("ns-velocity-msec", 0, true); // waypoints SGPropertyNode *wp_node = _gpsNode->getChild("wp", 0, true); @@ -420,6 +422,7 @@ GPS::clearOutput() _indicated_pos = SGGeod(); _last_vertical_speed = 0.0; _last_true_track = 0.0; + _lastEWVelocity = _lastNSVelocity = 0.0; _raim_node->setDoubleValue(0.0); _indicated_pos = SGGeod(); @@ -431,6 +434,8 @@ GPS::clearOutput() _tracking_bug_node->setDoubleValue(0); _true_bug_error_node->setDoubleValue(0); _magnetic_bug_error_node->setDoubleValue(0); + _northSouthVelocity->setDoubleValue(0.0); + _eastWestVelocity->setDoubleValue(0.0); } void @@ -545,9 +550,28 @@ GPS::updateBasicData(double dt) double vertical_speed_mpm = ((_indicated_pos.getElevationM() - _last_pos.getElevationM()) * 60 / dt); _last_vertical_speed = vertical_speed_mpm * SG_METER_TO_FEET; - speed_kt = fgGetLowPass(_last_speed_kts, speed_kt, dt/20.0); + speed_kt = fgGetLowPass(_last_speed_kts, speed_kt, dt/10.0); _last_speed_kts = speed_kt; + SGGeod g = _indicated_pos; + g.setLongitudeDeg(_last_pos.getLongitudeDeg()); + double northSouthM = SGGeodesy::distanceM(_last_pos, g); + northSouthM = copysign(northSouthM, _indicated_pos.getLatitudeDeg() - _last_pos.getLatitudeDeg()); + + double nsMSec = fgGetLowPass(_lastNSVelocity, northSouthM / dt, dt/2.0); + _lastNSVelocity = nsMSec; + _northSouthVelocity->setDoubleValue(nsMSec); + + + g = _indicated_pos; + g.setLatitudeDeg(_last_pos.getLatitudeDeg()); + double eastWestM = SGGeodesy::distanceM(_last_pos, g); + eastWestM = copysign(eastWestM, _indicated_pos.getLongitudeDeg() - _last_pos.getLongitudeDeg()); + + double ewMSec = fgGetLowPass(_lastEWVelocity, eastWestM / dt, dt/2.0); + _lastEWVelocity = ewMSec; + _eastWestVelocity->setDoubleValue(ewMSec); + double odometer = _odometer_node->getDoubleValue(); _odometer_node->setDoubleValue(odometer + distance_m * SG_METER_TO_NM); odometer = _trip_odometer_node->getDoubleValue(); diff --git a/src/Instrumentation/gps.hxx b/src/Instrumentation/gps.hxx index 24c8f50fb..56936974d 100644 --- a/src/Instrumentation/gps.hxx +++ b/src/Instrumentation/gps.hxx @@ -333,6 +333,8 @@ private: SGPropertyNode_ptr _trip_odometer_node; SGPropertyNode_ptr _true_bug_error_node; SGPropertyNode_ptr _magnetic_bug_error_node; + SGPropertyNode_ptr _eastWestVelocity; + SGPropertyNode_ptr _northSouthVelocity; SGPropertyNode_ptr _ref_navaid_id_node; SGPropertyNode_ptr _ref_navaid_bearing_node; @@ -358,6 +360,8 @@ private: double _last_speed_kts; double _last_true_track; double _last_vertical_speed; + double _lastEWVelocity; + double _lastNSVelocity; std::string _mode; GPSListener* _listener; From 4e24fbbb91cbb1ef94e0a50cc3aa061648bab2d4 Mon Sep 17 00:00:00 2001 From: jmt Date: Sat, 27 Mar 2010 11:37:25 +0000 Subject: [PATCH 04/13] GPS: enable switching to OBS/DTO mode with no valid scratch - use active waypoint. --- src/Instrumentation/gps.cxx | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index 4eee22a69..740c70a38 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -1325,21 +1325,18 @@ bool GPS::isScratchPositionValid() const void GPS::directTo() { - if (!isScratchPositionValid()) { - SG_LOG(SG_INSTR, SG_WARN, "invalid DTO lat/lon"); - return; + _wp0_position = _indicated_pos; + + if (isScratchPositionValid()) { + _wp1Ident = _scratchNode->getStringValue("ident"); + _wp1Name = _scratchNode->getStringValue("name"); + _wp1_position = _scratchPos; } - _wp0_position = _indicated_pos; - _wp1Ident = _scratchNode->getStringValue("ident"); - _wp1Name = _scratchNode->getStringValue("name"); - _wp1_position = _scratchPos; - _mode = "dto"; _desiredCourse = getLegMagCourse(); _desiredCourseNode->fireValueChanged(); clearScratch(); - wp1Changed(); } @@ -1609,17 +1606,14 @@ void GPS::addAirportToScratch(FGAirport* aAirport) void GPS::selectOBSMode() { - if (!isScratchPositionValid()) { - SG_LOG(SG_INSTR, SG_WARN, "invalid OBS lat/lon"); - return; + if (isScratchPositionValid()) { + _wp1Ident = _scratchNode->getStringValue("ident"); + _wp1Name = _scratchNode->getStringValue("name"); + _wp1_position = _scratchPos; } SG_LOG(SG_INSTR, SG_INFO, "GPS switching to OBS mode"); _mode = "obs"; - - _wp1Ident = _scratchNode->getStringValue("ident"); - _wp1Name = _scratchNode->getStringValue("name"); - _wp1_position = _scratchPos; _wp0_position = _indicated_pos; wp1Changed(); } From ee8437431a60877b18746512ba33c276dc24c1d8 Mon Sep 17 00:00:00 2001 From: jmt Date: Sat, 27 Mar 2010 12:18:51 +0000 Subject: [PATCH 05/13] Expose a flag indicating if the GPS is driving the AP (to give clearer user feedback when that is the case) --- src/Instrumentation/gps.cxx | 8 +++++++- src/Instrumentation/gps.hxx | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index 740c70a38..c44cd7110 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -303,6 +303,7 @@ GPS::init () fromFlag->alias(wp1_node->getChild("from-flag")); // autopilot drive properties + _apDrivingFlag = fgGetNode("/autopilot/settings/gps-driving-true-heading", true); _apTrueHeading = fgGetNode("/autopilot/settings/true-heading-deg",true); _apTargetAltitudeFt = fgGetNode("/autopilot/settings/target-altitude-ft", true); _apAltitudeLock = fgGetNode("/autopilot/locks/altitude", true); @@ -958,12 +959,17 @@ void GPS::updateRouteData() void GPS::driveAutopilot() { if (!_config.driveAutopilot() || !_realismSimpleGps->getBoolValue()) { + _apDrivingFlag->setBoolValue(false); return; } // compatability feature - allow the route-manager / GPS to drive the // generic autopilot heading hold *in leg mode only* - if (_mode == "leg") { + + bool drive = _mode == "leg"; + _apDrivingFlag->setBoolValue(drive); + + if (drive) { // FIXME: we want to set desired track, not heading, here _apTrueHeading->setDoubleValue(getWP1Bearing()); } diff --git a/src/Instrumentation/gps.hxx b/src/Instrumentation/gps.hxx index 56936974d..f1a1b4396 100644 --- a/src/Instrumentation/gps.hxx +++ b/src/Instrumentation/gps.hxx @@ -413,6 +413,7 @@ private: SGPropertyNode_ptr _realismSimpleGps; ///< should the GPS be simple or realistic? // autopilot drive properties + SGPropertyNode_ptr _apDrivingFlag; SGPropertyNode_ptr _apTrueHeading; SGPropertyNode_ptr _apTargetAltitudeFt; SGPropertyNode_ptr _apAltitudeLock; From e4c4a3a412806a8bc951c2b63df1696a5c862eaf Mon Sep 17 00:00:00 2001 From: jmt Date: Sat, 27 Mar 2010 16:55:33 +0000 Subject: [PATCH 06/13] MapWidget for the GUI, initial commit. --- projects/VC7.1/FlightGear.vcproj | 2 + projects/VC90/FlightGear/FlightGear.vcproj | 2 + src/GUI/Makefile.am | 3 +- src/GUI/MapWidget.cxx | 1613 ++++++++++++++++++++ src/GUI/MapWidget.hxx | 109 ++ src/GUI/dialog.cxx | 25 +- src/Instrumentation/navradio.cxx | 32 +- src/Instrumentation/navradio.hxx | 5 - src/Navaids/navrecord.cxx | 32 + src/Navaids/navrecord.hxx | 7 + 10 files changed, 1787 insertions(+), 43 deletions(-) create mode 100644 src/GUI/MapWidget.cxx create mode 100644 src/GUI/MapWidget.hxx diff --git a/projects/VC7.1/FlightGear.vcproj b/projects/VC7.1/FlightGear.vcproj index 388947077..54a86e94f 100755 --- a/projects/VC7.1/FlightGear.vcproj +++ b/projects/VC7.1/FlightGear.vcproj @@ -1828,6 +1828,8 @@ + + + + +#include // for std::sort +#include + +#include +#include +#include +#include +#include // for magVar julianDate +#include + +#include
+#include
+#include +#include +#include +#include +#include +#include +#include +#include
// fgGetKeyModifiers() + +const char* RULER_LEGEND_KEY = "ruler-legend"; + +/* equatorial and polar earth radius */ +const float rec = 6378137; // earth radius, equator (?) +const float rpol = 6356752.314f; // earth radius, polar (?) + +/************************************************************************ + some trigonometric helper functions + (translated more or less directly from Alexei Novikovs perl original) +*************************************************************************/ + +//Returns Earth radius at a given latitude (Ellipsoide equation with two equal axis) +static float earth_radius_lat( float lat ) +{ + double a = cos(lat)/rec; + double b = sin(lat)/rpol; + return 1.0f / sqrt( a * a + b * b ); +} + +/////////////////////////////////////////////////////////////////////////// + +static puBox makePuBox(int x, int y, int w, int h) +{ + puBox r; + r.min[0] = x; + r.min[1] = y; + r.max[0] = x + w; + r.max[1] = y + h; + return r; +} + +static bool puBoxIntersect(const puBox& a, const puBox& b) +{ + int x0 = SG_MAX2(a.min[0], b.min[0]); + int y0 = SG_MAX2(a.min[1], b.min[1]); + int x1 = SG_MIN2(a.max[0], b.max[0]); + int y1 = SG_MIN2(a.max[1], b.max[1]); + + return (x0 <= x1) && (y0 <= y1); +} + +class MapData; +typedef std::vector MapDataVec; + +class MapData +{ +public: + static const int HALIGN_LEFT = 1; + static const int HALIGN_CENTER = 2; + static const int HALIGN_RIGHT = 3; + + static const int VALIGN_TOP = 1 << 4; + static const int VALIGN_CENTER = 2 << 4; + static const int VALIGN_BOTTOM = 3 << 4; + + MapData(int priority) : + _dirtyText(true), + _age(0), + _priority(priority), + _width(0), + _height(0), + _offsetDir(HALIGN_LEFT | VALIGN_CENTER), + _offsetPx(10), + _dataVisible(false) + { + } + + void setLabel(const std::string& label) + { + if (label == _label) { + return; // common case, and saves invalidation + } + + _label = label; + _dirtyText = true; + } + + void setText(const std::string &text) + { + if (_rawText == text) { + return; // common case, and saves invalidation + } + + _rawText = text; + _dirtyText = true; + } + + void setDataVisible(bool vis) { + if (vis == _dataVisible) { + return; + } + + if (_rawText.empty()) { + vis = false; + } + + _dataVisible = vis; + _dirtyText = true; + } + + static void setFont(puFont f) + { + _font = f; + _fontHeight = f.getStringHeight(); + _fontDescender = f.getStringDescender(); + } + + static void setPalette(puColor* pal) + { + _palette = pal; + } + + void setPriority(int pri) + { + _priority = pri; + } + + int priority() const + { return _priority; } + + void setAnchor(const SGVec2d& anchor) + { + _anchor = anchor; + } + + void setOffset(int direction, int px) + { + if ((_offsetPx == px) && (_offsetDir == direction)) { + return; + } + + _dirtyOffset = true; + _offsetDir = direction; + _offsetPx = px; + } + + bool isClipped(const puBox& vis) const + { + validate(); + if ((_width < 1) || (_height < 1)) { + return true; + } + + return !puBoxIntersect(vis, box()); + } + + bool overlaps(const MapDataVec& l) const + { + validate(); + puBox b(box()); + + MapDataVec::const_iterator it; + for (it = l.begin(); it != l.end(); ++it) { + if (puBoxIntersect(b, (*it)->box())) { + return true; + } + } // of list iteration + + return false; + } + + puBox box() const + { + validate(); + return makePuBox( + _anchor.x() + _offset.x(), + _anchor.y() + _offset.y(), + _width, _height); + } + + void draw() + { + validate(); + + int xx = _anchor.x() + _offset.x(); + int yy = _anchor.y() + _offset.y(); + + if (_dataVisible) { + puBox box(makePuBox(0,0,_width, _height)); + int border = 1; + box.draw(xx, yy, PUSTYLE_DROPSHADOW, _palette, FALSE, border); + + // draw lines + int lineHeight = _fontHeight; + int xPos = xx + MARGIN; + int yPos = yy + _height - (lineHeight + MARGIN); + glColor3f(0.8, 0.8, 0.8); + + for (unsigned int ln=0; ln<_lines.size(); ++ln) { + _font.drawString(_lines[ln].c_str(), xPos, yPos); + yPos -= lineHeight + LINE_LEADING; + } + } else { + glColor3f(0.8, 0.8, 0.8); + _font.drawString(_label.c_str(), xx, yy + _fontDescender); + } + } + + void age() + { + ++_age; + } + + void resetAge() + { + _age = 0; + } + + bool isExpired() const + { return (_age > 100); } + + static bool order(MapData* a, MapData* b) + { + return a->_priority > b->_priority; + } +private: + void validate() const + { + if (!_dirtyText) { + if (_dirtyOffset) { + computeOffset(); + } + + return; + } + + if (_dataVisible) { + measureData(); + } else { + measureLabel(); + } + + computeOffset(); + _dirtyText = false; + } + + void measureData() const + { + _lines = simgear::strutils::split(_rawText, "\n"); + // measure text to find width and height + _width = -1; + _height = 0; + + for (unsigned int ln=0; ln<_lines.size(); ++ln) { + _height += _fontHeight; + if (ln > 0) { + _height += LINE_LEADING; + } + + int lw = _font.getStringWidth(_lines[ln].c_str()); + _width = std::max(_width, lw); + } // of line measurement + + if ((_width < 1) || (_height < 1)) { + // will be clipped + return; + } + + _height += MARGIN * 2; + _width += MARGIN * 2; + } + + void measureLabel() const + { + if (_label.empty()) { + _width = _height = -1; + return; + } + + _height = _fontHeight; + _width = _font.getStringWidth(_label.c_str()); + } + + void computeOffset() const + { + _dirtyOffset = false; + if ((_width <= 0) || (_height <= 0)) { + return; + } + + int hOffset = 0; + int vOffset = 0; + + switch (_offsetDir & 0x0f) { + default: + case HALIGN_LEFT: + hOffset = _offsetPx; + break; + + case HALIGN_CENTER: + hOffset = -(_width>>1); + break; + + case HALIGN_RIGHT: + hOffset = -(_offsetPx + _width); + break; + } + + switch (_offsetDir & 0xf0) { + default: + case VALIGN_TOP: + vOffset = -(_offsetPx + _height); + break; + + case VALIGN_CENTER: + vOffset = -(_height>>1); + break; + + case VALIGN_BOTTOM: + vOffset = _offsetPx; + break; + } + + _offset = SGVec2d(hOffset, vOffset); + } + + static const int LINE_LEADING = 3; + static const int MARGIN = 3; + + mutable bool _dirtyText; + mutable bool _dirtyOffset; + int _age; + std::string _rawText; + std::string _label; + mutable std::vector _lines; + int _priority; + mutable int _width, _height; + SGVec2d _anchor; + int _offsetDir; + int _offsetPx; + mutable SGVec2d _offset; + bool _dataVisible; + + static puFont _font; + static puColor* _palette; + static int _fontHeight; + static int _fontDescender; +}; + +puFont MapData::_font; +puColor* MapData::_palette; +int MapData::_fontHeight = 0; +int MapData::_fontDescender = 0; + +/////////////////////////////////////////////////////////////////////////// + +const int MAX_ZOOM = 16; +const int SHOW_DETAIL_ZOOM = 8; +const int CURSOR_PAN_STEP = 32; + +MapWidget::MapWidget(int x, int y, int maxX, int maxY) : + puObject(x,y,maxX, maxY) +{ + _route = static_cast(globals->get_subsystem("route-manager")); + _gps = fgGetNode("/instrumentation/gps"); + + _zoom = 6; + _width = maxX - x; + _height = maxY - y; + + MapData::setFont(legendFont); + MapData::setPalette(colour); + + _magVar = new SGMagVar(); +} + +MapWidget::~MapWidget() +{ + delete _magVar; +} + +void MapWidget::setProperty(SGPropertyNode_ptr prop) +{ + _root = prop; + _root->setBoolValue("centre-on-aircraft", true); + _root->setBoolValue("draw-data", false); + _root->setBoolValue("magnetic-headings", true); +} + +void MapWidget::setSize(int w, int h) +{ + puObject::setSize(w, h); + + _width = w; + _height = h; + +} + +void MapWidget::doHit( int button, int updown, int x, int y ) +{ + puObject::doHit(button, updown, x, y); + if (updown == PU_DRAG) { + handlePan(x, y); + return; + } + + if (button == 3) { // mouse-wheel up + zoomIn(); + } else if (button == 4) { // mouse-wheel down + zoomOut(); + } + + if (button != active_mouse_button) { + return; + } + + _hitLocation = SGVec2d(x - abox.min[0], y - abox.min[1]); + + if (updown == PU_UP) { + puDeactivateWidget(); + } else if (updown == PU_DOWN) { + puSetActiveWidget(this, x, y); + + if (fgGetKeyModifiers() & KEYMOD_CTRL) { + _clickGeod = unproject(_hitLocation - SGVec2d(_width>>1, _height>>1)); + } + } +} + +void MapWidget::handlePan(int x, int y) +{ + SGVec2d delta = SGVec2d(x, y) - _hitLocation; + pan(delta); + _hitLocation = SGVec2d(x,y); +} + +int MapWidget::checkKey (int key, int updown ) +{ + if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) { + return FALSE ; + } + + switch (key) + { + + case PU_KEY_UP: + pan(SGVec2d(0, -CURSOR_PAN_STEP)); + break; + + case PU_KEY_DOWN: + pan(SGVec2d(0, CURSOR_PAN_STEP)); + break ; + + case PU_KEY_LEFT: + pan(SGVec2d(CURSOR_PAN_STEP, 0)); + break; + + case PU_KEY_RIGHT: + pan(SGVec2d(-CURSOR_PAN_STEP, 0)); + break; + + case '-': + zoomOut(); + + break; + + case '=': + zoomIn(); + break; + + default : + return FALSE; + } + + return TRUE ; +} + +void MapWidget::pan(const SGVec2d& delta) +{ + _projectionCenter = unproject(-delta); +} + +void MapWidget::zoomIn() +{ + if (_zoom <= 0) { + return; + } + + --_zoom; + SG_LOG(SG_GENERAL, SG_INFO, "zoom is now:" << _zoom); +} + +void MapWidget::zoomOut() +{ + if (_zoom >= MAX_ZOOM) { + return; + } + + ++_zoom; + SG_LOG(SG_GENERAL, SG_INFO, "zoom is now:" << _zoom); +} + +void MapWidget::draw(int dx, int dy) +{ + _aircraft = SGGeod::fromDeg(fgGetDouble("/position/longitude-deg"), + fgGetDouble("/position/latitude-deg")); + _magneticHeadings = _root->getBoolValue("magnetic-headings"); + + if (_root->getBoolValue("centre-on-aircraft")) { + _projectionCenter = _aircraft; + _root->setBoolValue("centre-on-aircraft", false); + } + + double julianDate = globals->get_time_params()->getJD(); + _magVar->update(_projectionCenter, julianDate); + + SGGeod topLeft = unproject(SGVec2d(_width/2, _height/2)); + // compute draw range, including a fudge factor for ILSs and other 'long' + // symbols + _drawRangeNm = SGGeodesy::distanceNm(_projectionCenter, topLeft) + 10.0; + + bool aircraftUp = _root->getBoolValue("aircraft-heading-up"); + if (aircraftUp) { + _upHeading = fgGetDouble("/orientation/heading-deg"); + } else { + _upHeading = 0.0; + } + +// drawing operations + GLint sx = (int) abox.min[0], + sy = (int) abox.min[1]; + glScissor(dx + sx, dy + sy, _width, _height); + glEnable(GL_SCISSOR_TEST); + + glMatrixMode(GL_MODELVIEW); + glPushMatrix(); + // cetere drawing about the widget center (which is also the + // projection centre) + glTranslated(dx + sx + (_width/2), dy + sy + (_height/2), 0.0); + + drawLatLonGrid(); + + if (aircraftUp) { + int textHeight = legendFont.getStringHeight() + 5; + + // draw heading line + SGVec2d loc = project(_aircraft); + glColor3f(1.0, 1.0, 1.0); + drawLine(loc, SGVec2d(loc.x(), (_height / 2) - textHeight)); + + int displayHdg; + if (_magneticHeadings) { + displayHdg = (int) fgGetDouble("/orientation/heading-magnetic-deg"); + } else { + displayHdg = (int) _upHeading; + } + + double y = (_height / 2) - textHeight; + char buf[16]; + ::snprintf(buf, 16, "%d", displayHdg); + int sw = legendFont.getStringWidth(buf); + legendFont.drawString(buf, loc.x() - sw/2, y); + } + + drawAirports(); + drawNavaids(); + drawTraffic(); + drawGPSData(); + drawNavRadio(fgGetNode("/instrumentation/nav[0]", false)); + drawNavRadio(fgGetNode("/instrumentation/nav[1]", false)); + paintAircraftLocation(_aircraft); + paintRoute(); + paintRuler(); + + drawData(); + + glPopMatrix(); + glDisable(GL_SCISSOR_TEST); +} + +void MapWidget::paintRuler() +{ + if (_clickGeod == SGGeod()) { + return; + } + + SGVec2d acftPos = project(_aircraft); + SGVec2d clickPos = project(_clickGeod); + + glColor4f(0.0, 1.0, 1.0, 0.6); + drawLine(acftPos, clickPos); + + circleAtAlt(clickPos, 8, 10, 5); + + double dist, az, az2; + SGGeodesy::inverse(_aircraft, _clickGeod, az, az2, dist); + if (_magneticHeadings) { + az -= _magVar->get_magvar(); + SG_NORMALIZE_RANGE(az, 0.0, 360.0); + } + + char buffer[1024]; + ::snprintf(buffer, 1024, "%03d/%.1fnm", + SGMiscd::roundToInt(az), dist * SG_METER_TO_NM); + + MapData* d = getOrCreateDataForKey((void*) RULER_LEGEND_KEY); + d->setLabel(buffer); + d->setAnchor(clickPos); + d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15); + d->setPriority(20000); + + +} + +void MapWidget::paintAircraftLocation(const SGGeod& aircraftPos) +{ + SGVec2d loc = project(aircraftPos); + + double hdg = fgGetDouble("/orientation/heading-deg"); + + glLineWidth(2.0); + glColor4f(1.0, 1.0, 0.0, 1.0); + glPushMatrix(); + glTranslated(loc.x(), loc.y(), 0.0); + glRotatef(hdg - _upHeading, 0.0, 0.0, -1.0); + + const SGVec2d wingspan(12, 0); + const SGVec2d nose(0, 8); + const SGVec2d tail(0, -14); + const SGVec2d tailspan(4,0); + + drawLine(-wingspan, wingspan); + drawLine(nose, tail); + drawLine(tail - tailspan, tail + tailspan); + + glPopMatrix(); + glLineWidth(1.0); +} + +void MapWidget::paintRoute() +{ + if (_route->size() < 2) { + return; + } + +// first pass, draw the actual line + glLineWidth(2.0); + glBegin(GL_LINE_STRIP); + + SGVec2d prev = project(_route->get_waypoint(0).get_target()); + glVertex2d(prev.x(), prev.y()); + + for (int w=1; w < _route->size(); ++w) { + + SGVec2d p = project(_route->get_waypoint(w).get_target()); + + if (w < _route->currentWaypoint()) { + glColor4f(0.5, 0.5, 0.5, 0.7); + } else { + glColor4f(1.0, 0.0, 1.0, 1.0); + } + + glVertex2d(p.x(), p.y()); + + } + glEnd(); + + glLineWidth(1.0); +// second pass, draw waypoint symbols and data + for (int w=0; w < _route->size(); ++w) { + const SGWayPoint& wpt(_route->get_waypoint(w)); + SGVec2d p = project(wpt.get_target()); + glColor4f(1.0, 0.0, 1.0, 1.0); + circleAtAlt(p, 8, 12, 5); + + std::ostringstream legend; + legend << wpt.get_id(); + if (wpt.get_target_alt() > -9990.0) { + legend << '\n' << SGMiscd::roundToInt(wpt.get_target_alt()) << '\''; + } + + if (wpt.get_speed() > 0.0) { + legend << '\n' << SGMiscd::roundToInt(wpt.get_speed()) << "Kts"; + } + + MapData* d = getOrCreateDataForKey(reinterpret_cast(w * 2)); + d->setText(legend.str()); + d->setLabel(wpt.get_id()); + d->setAnchor(p); + d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15); + d->setPriority(w < _route->currentWaypoint() ? 9000 : 12000); + + if (w > 0) { + SGVec2d legMid = (prev + p) * 0.5; + std::ostringstream legLegend; + + double track = wpt.get_track(); + if (_magneticHeadings) { + track -= _magVar->get_magvar(); // show magnetic track for leg + } + + legLegend << SGMiscd::roundToInt(track) << " " + << SGMiscd::roundToInt(wpt.get_distance() * SG_METER_TO_NM) << "Nm"; + + MapData* ld = getOrCreateDataForKey(reinterpret_cast(w * 2 + 1)); + ld->setText(legLegend.str()); + ld->setAnchor(legMid); + ld->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 15); + ld->setPriority(w < _route->currentWaypoint() ? 8000 : 11000); + } // of draw leg data + + prev = p; + } // of second waypoint iteration +} + +/** + * Round a SGGeod to an arbitrary precision. + * For example, passing precision of 0.5 will round to the nearest 0.5 of + * a degree in both lat and lon - passing in 3.0 rounds to the nearest 3 degree + * multiple, and so on. + */ +static SGGeod roundGeod(double precision, const SGGeod& g) +{ + double lon = SGMiscd::round(g.getLongitudeDeg() / precision); + double lat = SGMiscd::round(g.getLatitudeDeg() / precision); + + return SGGeod::fromDeg(lon * precision, lat * precision); +} + +bool MapWidget::drawLineClipped(const SGVec2d& a, const SGVec2d& b) +{ + double minX = SGMiscd::min(a.x(), b.x()), + minY = SGMiscd::min(a.y(), b.y()), + maxX = SGMiscd::max(a.x(), b.x()), + maxY = SGMiscd::max(a.y(), b.y()); + + int hh = _height >> 1, hw = _width >> 1; + + if ((maxX < -hw) || (minX > hw) || (minY > hh) || (maxY < -hh)) { + return false; + } + + glVertex2dv(a.data()); + glVertex2dv(b.data()); + return true; +} + +SGVec2d MapWidget::gridPoint(int ix, int iy) +{ + int key = (ix + 0x7fff) | ((iy + 0x7fff) << 16); + GridPointCache::iterator it = _gridCache.find(key); + if (it != _gridCache.end()) { + return it->second; + } + + SGGeod gp = SGGeod::fromDeg( + _gridCenter.getLongitudeDeg() + ix * _gridSpacing, + _gridCenter.getLatitudeDeg() + iy * _gridSpacing); + + SGVec2d proj = project(gp); + _gridCache[key] = proj; + return proj; +} + +void MapWidget::drawLatLonGrid() +{ + _gridSpacing = 1.0; + _gridCenter = roundGeod(_gridSpacing, _projectionCenter); + _gridCache.clear(); + + int ix = 0; + int iy = 0; + + glColor4f(0.8, 0.8, 0.8, 0.4); + glBegin(GL_LINES); + bool didDraw; + do { + didDraw = false; + ++ix; + ++iy; + + for (int x = -ix; x < ix; ++x) { + didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x+1, -iy)); + didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x+1, iy)); + didDraw |= drawLineClipped(gridPoint(x, -iy), gridPoint(x, -iy + 1)); + didDraw |= drawLineClipped(gridPoint(x, iy), gridPoint(x, iy - 1)); + + } + + for (int y = -iy; y < iy; ++y) { + didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix, y+1)); + didDraw |= drawLineClipped(gridPoint(-ix, y), gridPoint(-ix + 1, y)); + didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix, y+1)); + didDraw |= drawLineClipped(gridPoint(ix, y), gridPoint(ix - 1, y)); + } + + if (ix > 30) { + break; + } + } while (didDraw); + + glEnd(); +} + +void MapWidget::drawGPSData() +{ + std::string gpsMode = _gps->getStringValue("mode"); + + SGGeod wp0Geod = SGGeod::fromDeg( + _gps->getDoubleValue("wp/wp[0]/longitude-deg"), + _gps->getDoubleValue("wp/wp[0]/latitude-deg")); + + SGGeod wp1Geod = SGGeod::fromDeg( + _gps->getDoubleValue("wp/wp[1]/longitude-deg"), + _gps->getDoubleValue("wp/wp[1]/latitude-deg")); + +// draw track line + double gpsTrackDeg = _gps->getDoubleValue("indicated-track-true-deg"); + double gpsSpeed = _gps->getDoubleValue("indicated-ground-speed-kt"); + double az2; + + if (gpsSpeed > 3.0) { // only draw track line if valid + SGGeod trackRadial; + SGGeodesy::direct(_aircraft, gpsTrackDeg, _drawRangeNm * SG_NM_TO_METER, trackRadial, az2); + + glColor4f(1.0, 1.0, 0.0, 1.0); + glEnable(GL_LINE_STIPPLE); + glLineStipple(1, 0x00FF); + drawLine(project(_aircraft), project(trackRadial)); + glDisable(GL_LINE_STIPPLE); + } + + if (gpsMode == "dto") { + SGVec2d wp0Pos = project(wp0Geod); + SGVec2d wp1Pos = project(wp1Geod); + + glColor4f(1.0, 0.0, 1.0, 1.0); + drawLine(wp0Pos, wp1Pos); + + } + + if (_gps->getBoolValue("scratch/valid")) { + // draw scratch data + + } +} + +class MapAirportFilter : public FGAirport::AirportFilter +{ +public: + MapAirportFilter(SGPropertyNode_ptr nd) + { + _heliports = nd->getBoolValue("show-heliports", false); + _hardRunwaysOnly = nd->getBoolValue("hard-surfaced-airports", true); + _minLengthFt = nd->getDoubleValue("min-runway-length-ft", 2000.0); + } + + virtual FGPositioned::Type maxType() const { + return _heliports ? FGPositioned::HELIPORT : FGPositioned::AIRPORT; + } + + virtual bool passAirport(FGAirport* aApt) const { + if (_hardRunwaysOnly) { + return aApt->hasHardRunwayOfLengthFt(_minLengthFt); + } + + return true; + } + +private: + bool _heliports; + bool _hardRunwaysOnly; + double _minLengthFt; +}; + +void MapWidget::drawAirports() +{ + MapAirportFilter af(_root); + FGPositioned::List apts = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &af); + for (unsigned int i=0; itype() == FGPositioned::FIX)) { + // ignore fixes which end in digits - expirmental + if (isdigit(aPos->ident()[3]) && isdigit(aPos->ident()[4])) { + return false; + } + } + + return true; + } + + virtual FGPositioned::Type minType() const { + return _fixes ? FGPositioned::FIX : FGPositioned::VOR; + } + + virtual FGPositioned::Type maxType() const { + return _navaids ? FGPositioned::NDB : FGPositioned::FIX; + } + +private: + bool _fixes, _navaids; +}; + +void MapWidget::drawNavaids() +{ + bool fixes = _root->getBoolValue("draw-fixes"); + NavaidFilter f(fixes, _root->getBoolValue("draw-navaids")); + + if (f.minType() <= f.maxType()) { + FGPositioned::List navs = FGPositioned::findWithinRange(_projectionCenter, _drawRangeNm, &f); + + glLineWidth(1.0); + for (unsigned int i=0; itype(); + if (ty == FGPositioned::NDB) { + drawNDB(false, (FGNavRecord*) navs[i].get()); + } else if (ty == FGPositioned::VOR) { + drawVOR(false, (FGNavRecord*) navs[i].get()); + } else if (ty == FGPositioned::FIX) { + drawFix((FGFix*) navs[i].get()); + } + } // of navaid iteration + } // of navaids || fixes are drawn test +} + +void MapWidget::drawNDB(bool tuned, FGNavRecord* ndb) +{ + SGVec2d pos = project(ndb->geod()); + + if (tuned) { + glColor3f(0.0, 1.0, 1.0); + } else { + glColor3f(0.0, 0.0, 0.0); + } + + glEnable(GL_LINE_STIPPLE); + glLineStipple(1, 0x00FF); + circleAt(pos, 20, 6); + circleAt(pos, 20, 10); + glDisable(GL_LINE_STIPPLE); + + if (validDataForKey(ndb)) { + setAnchorForKey(ndb, pos); + return; + } + + char buffer[1024]; + ::snprintf(buffer, 1024, "%s\n%s %3.0fKhz", + ndb->name().c_str(), ndb->ident().c_str(),ndb->get_freq()/100.0); + + MapData* d = createDataForKey(ndb); + d->setPriority(40); + d->setLabel(ndb->ident()); + d->setText(buffer); + d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 10); + d->setAnchor(pos); + +} + +void MapWidget::drawVOR(bool tuned, FGNavRecord* vor) +{ + SGVec2d pos = project(vor->geod()); + if (tuned) { + glColor3f(0.0, 1.0, 1.0); + } else { + glColor3f(0.0, 0.0, 1.0); + } + + circleAt(pos, 6, 8); + + if (validDataForKey(vor)) { + setAnchorForKey(vor, pos); + return; + } + + char buffer[1024]; + ::snprintf(buffer, 1024, "%s\n%s %6.3fMhz", + vor->name().c_str(), vor->ident().c_str(), + vor->get_freq() / 100.0); + + MapData* d = createDataForKey(vor); + d->setText(buffer); + d->setLabel(vor->ident()); + d->setPriority(tuned ? 10000 : 100); + d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12); + d->setAnchor(pos); +} + +void MapWidget::drawFix(FGFix* fix) +{ + SGVec2d pos = project(fix->geod()); + glColor3f(0.0, 0.0, 0.0); + circleAt(pos, 3, 6); + + if (_zoom > SHOW_DETAIL_ZOOM) { + return; // hide fix labels beyond a certain zoom level + } + + if (validDataForKey(fix)) { + setAnchorForKey(fix, pos); + return; + } + + MapData* d = createDataForKey(fix); + d->setLabel(fix->ident()); + d->setPriority(20); + d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10); + d->setAnchor(pos); +} + +void MapWidget::drawNavRadio(SGPropertyNode_ptr radio) +{ + if (!radio || radio->getBoolValue("slaved-to-gps", false) + || !radio->getBoolValue("in-range", false)) { + return; + } + + if (radio->getBoolValue("nav-loc", false)) { + drawTunedLocalizer(radio); + } + + // identify the tuned station - unfortunately we don't get lat/lon directly, + // need to do the frequency search again + double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0); + FGNavRecord* nav = globals->get_navlist()->findByFreq(mhz, _aircraft); + if (!nav || (nav->ident() != radio->getStringValue("nav-id"))) { + // mismatch between navradio selection logic and ours! + return; + } + + glLineWidth(1.0); + drawVOR(true, nav); + + SGVec2d pos = project(nav->geod()); + SGGeod range; + double az2; + double trueRadial = radio->getDoubleValue("radials/target-radial-deg"); + SGGeodesy::direct(nav->geod(), trueRadial, nav->get_range() * SG_NM_TO_METER, range, az2); + SGVec2d prange = project(range); + + SGVec2d norm = normalize(prange - pos); + SGVec2d perp(norm.y(), -norm.x()); + + circleAt(pos, 64, length(prange - pos)); + drawLine(pos, prange); + +// draw to/from arrows + SGVec2d midPoint = (pos + prange) * 0.5; + if (radio->getBoolValue("from-flag")) { + norm = -norm; + perp = -perp; + } + + int sz = 10; + SGVec2d arrowB = midPoint - (norm * sz) + (perp * sz); + SGVec2d arrowC = midPoint - (norm * sz) - (perp * sz); + drawLine(midPoint, arrowB); + drawLine(arrowB, arrowC); + drawLine(arrowC, midPoint); + + drawLine(pos, (2 * pos) - prange); // reciprocal radial +} + +void MapWidget::drawTunedLocalizer(SGPropertyNode_ptr radio) +{ + double mhz = radio->getDoubleValue("frequencies/selected-mhz", 0.0); + FGNavRecord* loc = globals->get_loclist()->findByFreq(mhz, _aircraft); + if (!loc || (loc->ident() != radio->getStringValue("nav-id"))) { + // mismatch between navradio selection logic and ours! + return; + } + + if (loc->runway()) { + drawILS(true, loc->runway()); + } +} + +/* +void MapWidget::drawObstacle(FGPositioned* obs) +{ + SGVec2d pos = project(obs->geod()); + glColor3f(0.0, 0.0, 0.0); + glLineWidth(2.0); + drawLine(pos, pos + SGVec2d()); +} +*/ + +void MapWidget::drawAirport(FGAirport* apt) +{ + // draw tower location + SGVec2d towerPos = project(apt->getTowerLocation()); + + if (_zoom <= SHOW_DETAIL_ZOOM) { + glColor3f(1.0, 1.0, 1.0); + glLineWidth(1.0); + + drawLine(towerPos + SGVec2d(3, 0), towerPos + SGVec2d(3, 10)); + drawLine(towerPos + SGVec2d(-3, 0), towerPos + SGVec2d(-3, 10)); + drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(-3, 10)); + drawLine(towerPos + SGVec2d(6, 20), towerPos + SGVec2d(3, 10)); + drawLine(towerPos + SGVec2d(-6, 20), towerPos + SGVec2d(6, 20)); + } + + if (validDataForKey(apt)) { + setAnchorForKey(apt, towerPos); + } else { + char buffer[1024]; + ::snprintf(buffer, 1024, "%s\n%s", + apt->ident().c_str(), apt->name().c_str()); + + MapData* d = createDataForKey(apt); + d->setText(buffer); + d->setLabel(apt->ident()); + d->setPriority(100 + scoreAirportRunways(apt)); + d->setOffset(MapData::VALIGN_TOP | MapData::HALIGN_CENTER, 6); + d->setAnchor(towerPos); + } + + if (_zoom > SHOW_DETAIL_ZOOM) { + return; + } + + for (unsigned int r=0; rnumRunways(); ++r) { + FGRunway* rwy = apt->getRunwayByIndex(r); + if (!rwy->isReciprocal()) { + drawRunwayPre(rwy); + } + } + + for (unsigned int r=0; rnumRunways(); ++r) { + FGRunway* rwy = apt->getRunwayByIndex(r); + if (!rwy->isReciprocal()) { + drawRunway(rwy); + } + + if (rwy->ILS()) { + drawILS(false, rwy); + } + } // of runway iteration + +} + +int MapWidget::scoreAirportRunways(FGAirport* apt) +{ + bool needHardSurface = _root->getBoolValue("hard-surfaced-airports", true); + double minLength = _root->getDoubleValue("min-runway-length-ft", 2000.0); + + int score = 0; + unsigned int numRunways(apt->numRunways()); + for (unsigned int r=0; rgetRunwayByIndex(r); + if (rwy->isReciprocal()) { + continue; + } + + if (needHardSurface && !rwy->isHardSurface()) { + continue; + } + + if (rwy->lengthFt() < minLength) { + continue; + } + + int scoreLength = SGMiscd::roundToInt(rwy->lengthFt() / 200.0); + score += scoreLength; + } // of runways iteration + + return score; +} + +void MapWidget::drawRunwayPre(FGRunway* rwy) +{ + SGVec2d p1 = project(rwy->begin()); + SGVec2d p2 = project(rwy->end()); + + glLineWidth(4.0); + glColor3f(1.0, 0.0, 1.0); + drawLine(p1, p2); +} + +void MapWidget::drawRunway(FGRunway* rwy) +{ + // line for runway + // optionally show active, stopway, etc + // in legend, show published heading and length + // and threshold elevation + + SGVec2d p1 = project(rwy->begin()); + SGVec2d p2 = project(rwy->end()); + glLineWidth(2.0); + glColor3f(1.0, 1.0, 1.0); + SGVec2d inset = normalize(p2 - p1) * 2; + + drawLine(p1 + inset, p2 - inset); + + if (validDataForKey(rwy)) { + setAnchorForKey(rwy, (p1 + p2) * 0.5); + return; + } + + char buffer[1024]; + ::snprintf(buffer, 1024, "%s/%s\n%3.0f/%3.0f\n%.0f'", + rwy->ident().c_str(), + rwy->reciprocalRunway()->ident().c_str(), + rwy->headingDeg(), + rwy->reciprocalRunway()->headingDeg(), + rwy->lengthFt()); + + MapData* d = createDataForKey(rwy); + d->setText(buffer); + d->setLabel(rwy->ident() + "/" + rwy->reciprocalRunway()->ident()); + d->setPriority(50); + d->setOffset(MapData::HALIGN_CENTER | MapData::VALIGN_BOTTOM, 12); + d->setAnchor((p1 + p2) * 0.5); +} + +void MapWidget::drawILS(bool tuned, FGRunway* rwy) +{ + // arrow, tip centered on the landing threshold + // using LOC transmitter position would be more accurate, but + // is visually cluttered + // arrow width is based upon the computed localizer width + + FGNavRecord* loc = rwy->ILS(); + double halfBeamWidth = loc->localizerWidth() * 0.5; + SGVec2d t = project(rwy->threshold()); + SGGeod locEnd; + double rangeM = loc->get_range() * SG_NM_TO_METER; + double radial = loc->get_multiuse(); + SG_NORMALIZE_RANGE(radial, 0.0, 360.0); + double az2; + +// compute the three end points at the widge end of the arrow + SGGeodesy::direct(loc->geod(), radial, -rangeM, locEnd, az2); + SGVec2d endCentre = project(locEnd); + + SGGeodesy::direct(loc->geod(), radial + halfBeamWidth, -rangeM * 1.1, locEnd, az2); + SGVec2d endR = project(locEnd); + + SGGeodesy::direct(loc->geod(), radial - halfBeamWidth, -rangeM * 1.1, locEnd, az2); + SGVec2d endL = project(locEnd); + +// outline two triangles + glLineWidth(1.0); + if (tuned) { + glColor3f(0.0, 1.0, 1.0); + } else { + glColor3f(0.0, 0.0, 1.0); + } + + glBegin(GL_LINE_LOOP); + glVertex2dv(t.data()); + glVertex2dv(endCentre.data()); + glVertex2dv(endL.data()); + glEnd(); + glBegin(GL_LINE_LOOP); + glVertex2dv(t.data()); + glVertex2dv(endCentre.data()); + glVertex2dv(endR.data()); + glEnd(); +} + +void MapWidget::drawTraffic() +{ + if (!_root->getBoolValue("draw-traffic")) { + return; + } + + if (_zoom > SHOW_DETAIL_ZOOM) { + return; + } + + const SGPropertyNode* ai = fgGetNode("/ai/models", true); + + for (int i = 0; i < ai->nChildren(); ++i) { + const SGPropertyNode *model = ai->getChild(i); + // skip bad or dead entries + if (!model || model->getIntValue("id", -1) < 0) { + continue; + } + + const std::string& name(model->getName()); + SGGeod pos = SGGeod::fromDegFt( + model->getDoubleValue("position/longitude-deg"), + model->getDoubleValue("position/latitude-deg"), + model->getDoubleValue("position/altitude-ft")); + + double dist = SGGeodesy::distanceNm(_projectionCenter, pos); + if (dist > _drawRangeNm) { + continue; + } + + double heading = model->getDoubleValue("orientation/true-heading-deg"); + if ((name == "aircraft") || (name == "multiplayer") || + (name == "wingman") || (name == "tanker")) { + drawAIAircraft(model, pos, heading); + } else if ((name == "ship") || (name == "carrier") || (name == "escort")) { + drawAIShip(model, pos, heading); + } + } // of ai/models iteration +} + +void MapWidget::drawAIAircraft(const SGPropertyNode* model, const SGGeod& pos, double hdg) +{ + + SGVec2d p = project(pos); + + glColor3f(0.0, 0.0, 0.0); + glLineWidth(2.0); + circleAt(p, 4, 6.0); // black diamond + +// draw heading vector + int speedKts = static_cast(model->getDoubleValue("velocities/true-airspeed-kt")); + if (speedKts > 1) { + glLineWidth(1.0); + + const double dt = 15.0 / (3600.0); // 15 seconds look-ahead + double distanceM = speedKts * SG_NM_TO_METER * dt; + + SGGeod advance; + double az2; + SGGeodesy::direct(pos, hdg, distanceM, advance, az2); + + drawLine(p, project(advance)); + } + + if (validDataForKey((void*) model)) { + setAnchorForKey((void*) model, p); + return; + } + + // draw callsign / altitude / speed + + + char buffer[1024]; + ::snprintf(buffer, 1024, "%s\n%d'\n%dkts", + model->getStringValue("callsign", "<>"), + static_cast(pos.getElevationFt() / 50.0) * 50, + speedKts); + + MapData* d = createDataForKey((void*) model); + d->setText(buffer); + d->setLabel(model->getStringValue("callsign", "<>")); + d->setPriority(speedKts > 5 ? 60 : 10); // low priority for parked aircraft + d->setOffset(MapData::VALIGN_CENTER | MapData::HALIGN_LEFT, 10); + d->setAnchor(p); + +} + +void MapWidget::drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg) +{ + +} + +SGVec2d MapWidget::project(const SGGeod& geod) const +{ + // Sanson-Flamsteed projection, relative to the projection center + double r = earth_radius_lat(geod.getLatitudeRad()); + double lonDiff = geod.getLongitudeRad() - _projectionCenter.getLongitudeRad(), + latDiff = geod.getLatitudeRad() - _projectionCenter.getLatitudeRad(); + + SGVec2d p = SGVec2d(cos(geod.getLatitudeRad()) * lonDiff, latDiff) * r * currentScale(); + +// rotate as necessary + double cost = cos(_upHeading * SG_DEGREES_TO_RADIANS), + sint = sin(_upHeading * SG_DEGREES_TO_RADIANS); + double rx = cost * p.x() - sint * p.y(); + double ry = sint * p.x() + cost * p.y(); + return SGVec2d(rx, ry); +} + +SGGeod MapWidget::unproject(const SGVec2d& p) const +{ + // unrotate, if necessary + double cost = cos(-_upHeading * SG_DEGREES_TO_RADIANS), + sint = sin(-_upHeading * SG_DEGREES_TO_RADIANS); + SGVec2d ur(cost * p.x() - sint * p.y(), + sint * p.x() + cost * p.y()); + + double r = earth_radius_lat(_projectionCenter.getLatitudeRad()); + SGVec2d unscaled = ur * (1.0 / (currentScale() * r)); + + double lat = unscaled.y() + _projectionCenter.getLatitudeRad(); + double lon = (unscaled.x() / cos(lat)) + _projectionCenter.getLongitudeRad(); + + return SGGeod::fromRad(lon, lat); +} + +double MapWidget::currentScale() const +{ + return 1.0 / pow(2.0, _zoom); +} + +void MapWidget::circleAt(const SGVec2d& center, int nSides, double r) +{ + glBegin(GL_LINE_LOOP); + double advance = (SGD_PI * 2) / nSides; + glVertex2d(center.x(), center.y() + r); + double t=advance; + for (int i=1; i lines(simgear::strutils::split(t, "\n")); + const int LINE_LEADING = 4; + const int MARGIN = 4; + +// measure + int maxWidth = -1, totalHeight = 0; + int lineHeight = legendFont.getStringHeight(); + + for (unsigned int ln=0; ln 0) { + totalHeight += LINE_LEADING; + } + + int lw = legendFont.getStringWidth(lines[ln].c_str()); + maxWidth = std::max(maxWidth, lw); + } // of line measurement + + if (maxWidth < 0) { + return; // all lines are empty, don't draw + } + + totalHeight += MARGIN * 2; + +// draw box + puBox box; + box.min[0] = 0; + box.min[1] = -totalHeight; + box.max[0] = maxWidth + (MARGIN * 2); + box.max[1] = 0; + int border = 1; + box.draw (pos.x(), pos.y(), PUSTYLE_DROPSHADOW, colour, FALSE, border); + +// draw lines + int xPos = pos.x() + MARGIN; + int yPos = pos.y() - (lineHeight + MARGIN); + glColor3f(0.8, 0.8, 0.8); + + for (unsigned int ln=0; ln> 1, + hh = _height >> 1; + puBox visBox(makePuBox(-hw, -hh, _width, _height)); + + unsigned int d = 0; + int drawn = 0; + std::vector drawQueue; + + bool drawData = _root->getBoolValue("draw-data"); + const int MAX_DRAW_DATA = 25; + const int MAX_DRAW = 50; + + for (; (d < _dataQueue.size()) && (drawn < MAX_DRAW); ++d) { + MapData* md = _dataQueue[d]; + md->setDataVisible(drawData); + + if (md->isClipped(visBox)) { + continue; + } + + if (md->overlaps(drawQueue)) { + if (drawData) { // overlapped with data, let's try just the label + md->setDataVisible(false); + if (md->overlaps(drawQueue)) { + continue; + } + } else { + continue; + } + } // of overlaps case + + drawQueue.push_back(md); + ++drawn; + if (drawData && (drawn >= MAX_DRAW_DATA)) { + drawData = false; + } + } + + // draw lowest-priority first, so higher-priorty items appear on top + std::vector::reverse_iterator r; + for (r = drawQueue.rbegin(); r!= drawQueue.rend(); ++r) { + (*r)->draw(); + } + + _dataQueue.clear(); + KeyDataMap::iterator it = _mapData.begin(); + for (; it != _mapData.end(); ) { + it->second->age(); + if (it->second->isExpired()) { + delete it->second; + KeyDataMap::iterator cur = it++; + _mapData.erase(cur); + } else { + ++it; + } + } // of expiry iteration +} + +bool MapWidget::validDataForKey(void* key) +{ + KeyDataMap::iterator it = _mapData.find(key); + if (it == _mapData.end()) { + return false; // no valid data for the key! + } + + it->second->resetAge(); // mark data as valid this frame + _dataQueue.push_back(it->second); + return true; +} + +void MapWidget::setAnchorForKey(void* key, const SGVec2d& anchor) +{ + KeyDataMap::iterator it = _mapData.find(key); + if (it == _mapData.end()) { + throw sg_exception("no valid data for key!"); + } + + it->second->setAnchor(anchor); +} + +MapData* MapWidget::getOrCreateDataForKey(void* key) +{ + KeyDataMap::iterator it = _mapData.find(key); + if (it == _mapData.end()) { + return createDataForKey(key); + } + + it->second->resetAge(); // mark data as valid this frame + _dataQueue.push_back(it->second); + return it->second; +} + +MapData* MapWidget::createDataForKey(void* key) +{ + KeyDataMap::iterator it = _mapData.find(key); + if (it != _mapData.end()) { + throw sg_exception("duplicate data requested for key!"); + } + + MapData* d = new MapData(0); + _mapData[key] = d; + _dataQueue.push_back(d); + d->resetAge(); + return d; +} diff --git a/src/GUI/MapWidget.hxx b/src/GUI/MapWidget.hxx new file mode 100644 index 000000000..f2722e327 --- /dev/null +++ b/src/GUI/MapWidget.hxx @@ -0,0 +1,109 @@ +#ifndef GUI_MAPWIDGET_HXX +#define GUI_MAPWIDGET_HXX + +#include +#include +#include + +#include + +#include "dialog.hxx" // for GUI_ID + +// forward decls +class FGRouteMgr; +class FGRunway; +class FGAirport; +class FGNavRecord; +class FGFix; +class MapData; +class SGMagVar; + +class MapWidget : public puObject +{ +public: + MapWidget(int x, int y, int width, int height); + virtual ~MapWidget(); + + virtual void setSize(int width, int height); + virtual void doHit( int button, int updown, int x, int y ) ; + virtual void draw( int dx, int dy ) ; + virtual int checkKey(int key, int updown); + + void setProperty(SGPropertyNode_ptr prop); +private: + void handlePan(int x, int y); + + void pan(const SGVec2d& delta); + void zoomIn(); + void zoomOut(); + + void paintAircraftLocation(const SGGeod& aircraftPos); + void paintRoute(); + void paintRuler(); + + void drawGPSData(); + void drawNavRadio(SGPropertyNode_ptr radio); + void drawTunedLocalizer(SGPropertyNode_ptr radio); + + void drawLatLonGrid(); + SGVec2d gridPoint(int ix, int iy); + bool drawLineClipped(const SGVec2d& a, const SGVec2d& b); + + void drawAirports(); + void drawAirport(FGAirport* apt); + int scoreAirportRunways(FGAirport* apt); + void drawRunwayPre(FGRunway* rwy); + void drawRunway(FGRunway* rwy); + void drawILS(bool tuned, FGRunway* rwy); + + void drawNavaids(); + void drawNDB(bool tuned, FGNavRecord* nav); + void drawVOR(bool tuned, FGNavRecord* nav); + void drawFix(FGFix* fix); + + void drawTraffic(); + void drawAIAircraft(const SGPropertyNode* model, const SGGeod& pos, double hdg); + void drawAIShip(const SGPropertyNode* model, const SGGeod& pos, double hdg); + + void drawData(); + bool validDataForKey(void* key); + MapData* getOrCreateDataForKey(void* key); + MapData* createDataForKey(void* key); + void setAnchorForKey(void* key, const SGVec2d& anchor); + + SGVec2d project(const SGGeod& geod) const; + SGGeod unproject(const SGVec2d& p) const; + double currentScale() const; + + void circleAt(const SGVec2d& center, int nSides, double r); + void circleAtAlt(const SGVec2d& center, int nSides, double r, double r2); + void drawLine(const SGVec2d& p1, const SGVec2d& p2); + void drawLegendBox(const SGVec2d& pos, const std::string& t); + + int _width, _height; + int _zoom; + double _drawRangeNm; + double _upHeading; // true heading corresponding to +ve y-axis + bool _magneticHeadings; + + SGGeod _projectionCenter; + SGGeod _aircraft; + SGGeod _clickGeod; + SGVec2d _hitLocation; + FGRouteMgr* _route; + SGPropertyNode_ptr _root; + SGPropertyNode_ptr _gps; + + typedef std::map KeyDataMap; + KeyDataMap _mapData; + std::vector _dataQueue; + + SGMagVar* _magVar; + + typedef std::map GridPointCache; + GridPointCache _gridCache; + double _gridSpacing; + SGGeod _gridCenter; +}; + +#endif // of GUI_MAPWIDGET_HXX diff --git a/src/GUI/dialog.cxx b/src/GUI/dialog.cxx index 698268ce9..fbf758b29 100644 --- a/src/GUI/dialog.cxx +++ b/src/GUI/dialog.cxx @@ -14,6 +14,7 @@ #include "property_list.hxx" #include "layout.hxx" #include "WaypointList.hxx" +#include "MapWidget.hxx" enum format_type { f_INVALID, f_INT, f_LONG, f_FLOAT, f_DOUBLE, f_STRING }; static const int FORMAT_BUFSIZE = 255; @@ -815,7 +816,10 @@ FGDialog::makeObject (SGPropertyNode *props, int parentWidth, int parentHeight) setupObject(obj, props); setColor(obj, props); return obj; - + } else if (type == "map") { + MapWidget* mapWidget = new MapWidget(x, y, x + width, y + height); + setupObject(mapWidget, props); + return mapWidget; } else if (type == "combo") { fgComboBox *obj = new fgComboBox(x, y, x + width, y + height, props, props->getBoolValue("editable", false)); @@ -953,12 +957,21 @@ FGDialog::setupObject (puObject *object, SGPropertyNode *props) name = ""; const char *propname = props->getStringValue("property"); SGPropertyNode_ptr node = fgGetNode(propname, true); - copy_to_pui(node, object); + if (type == "map") { + // mapWidget binds to a sub-tree of properties, and + // ignroes the puValue mechanism, so special case things here + MapWidget* mw = static_cast(object); + mw->setProperty(node); + } else { + // normal widget, creating PropertyObject + copy_to_pui(node, object); + PropertyObject *po = new PropertyObject(name, object, node); + _propertyObjects.push_back(po); + if (props->getBoolValue("live")) + _liveObjects.push_back(po); + } + - PropertyObject *po = new PropertyObject(name, object, node); - _propertyObjects.push_back(po); - if (props->getBoolValue("live")) - _liveObjects.push_back(po); } SGPropertyNode *dest = fgGetNode("/sim/bindings/gui", true); diff --git a/src/Instrumentation/navradio.cxx b/src/Instrumentation/navradio.cxx index 60eb08856..b3d621b7d 100644 --- a/src/Instrumentation/navradio.cxx +++ b/src/Instrumentation/navradio.cxx @@ -891,7 +891,7 @@ void FGNavRadio::search() } else { // ILS or LOC _gs = globals->get_gslist()->findByFreq(freq, pos); has_gs_node->setBoolValue(_gs != NULL); - _localizerWidth = localizerWidth(nav); + _localizerWidth = nav->localizerWidth(); twist = 0.0; effective_range = nav->get_range(); @@ -941,36 +941,6 @@ void FGNavRadio::search() id_c4_node->setIntValue( (int)identBuffer[3] ); } -double FGNavRadio::localizerWidth(FGNavRecord* aLOC) -{ - FGRunway* rwy = aLOC->runway(); - assert(rwy); - - SGVec3d thresholdCart(SGVec3d::fromGeod(rwy->threshold())); - double axisLength = dist(aLOC->cart(), thresholdCart); - double landingLength = dist(thresholdCart, SGVec3d::fromGeod(rwy->end())); - -// Reference: http://dcaa.slv.dk:8000/icaodocs/ -// ICAO standard width at threshold is 210 m = 689 feet = approx 700 feet. -// ICAO 3.1.1 half course = DDM = 0.0775 -// ICAO 3.1.3.7.1 Sensitivity 0.00145 DDM/m at threshold -// implies peg-to-peg of 214 m ... we will stick with 210. -// ICAO 3.1.3.7.1 "Course sector angle shall not exceed 6 degrees." - -// Very short runway: less than 1200 m (4000 ft) landing length: - if (landingLength < 1200.0) { -// ICAO fudges localizer sensitivity for very short runways. -// This produces a non-monotonic sensitivity-versus length relation. - axisLength += 1050.0; - } - -// Example: very short: San Diego KMYF (Montgomery Field) ILS RWY 28R -// Example: short: Tom's River KMJX (Robert J. Miller) ILS RWY 6 -// Example: very long: Denver KDEN (Denver) ILS RWY 16R - double raw_width = 210.0 / axisLength * SGD_RADIANS_TO_DEGREES; - return raw_width < 6.0? raw_width : 6.0; -} - void FGNavRadio::audioNavidChanged() { if (_sgr->exists(nav_fx_name)) { diff --git a/src/Instrumentation/navradio.hxx b/src/Instrumentation/navradio.hxx index 5cc368f1b..f272f6700 100644 --- a/src/Instrumentation/navradio.hxx +++ b/src/Instrumentation/navradio.hxx @@ -191,11 +191,6 @@ class FGNavRadio : public SGSubsystem, public SGPropertyChangeListener void clearOutputs(); - /** - * Compute the localizer width in degrees - see implementation for - * more information on the relevant standards and formulae. - */ - double localizerWidth(FGNavRecord* aLOC); FGNavRecord* findPrimaryNavaid(const SGGeod& aPos, double aFreqMHz); /// accessor for tied, read-only 'operable' property diff --git a/src/Navaids/navrecord.cxx b/src/Navaids/navrecord.cxx index e75339804..041c12f1a 100644 --- a/src/Navaids/navrecord.cxx +++ b/src/Navaids/navrecord.cxx @@ -181,6 +181,38 @@ void FGNavRecord::alignLocaliserWithRunway(double aThreshold) } } +double FGNavRecord::localizerWidth() const +{ + if (!mRunway) { + return 6.0; + } + + SGVec3d thresholdCart(SGVec3d::fromGeod(mRunway->threshold())); + double axisLength = dist(cart(), thresholdCart); + double landingLength = dist(thresholdCart, SGVec3d::fromGeod(mRunway->end())); + +// Reference: http://dcaa.slv.dk:8000/icaodocs/ +// ICAO standard width at threshold is 210 m = 689 feet = approx 700 feet. +// ICAO 3.1.1 half course = DDM = 0.0775 +// ICAO 3.1.3.7.1 Sensitivity 0.00145 DDM/m at threshold +// implies peg-to-peg of 214 m ... we will stick with 210. +// ICAO 3.1.3.7.1 "Course sector angle shall not exceed 6 degrees." + +// Very short runway: less than 1200 m (4000 ft) landing length: + if (landingLength < 1200.0) { +// ICAO fudges localizer sensitivity for very short runways. +// This produces a non-monotonic sensitivity-versus length relation. + axisLength += 1050.0; + } + +// Example: very short: San Diego KMYF (Montgomery Field) ILS RWY 28R +// Example: short: Tom's River KMJX (Robert J. Miller) ILS RWY 6 +// Example: very long: Denver KDEN (Denver) ILS RWY 16R + double raw_width = 210.0 / axisLength * SGD_RADIANS_TO_DEGREES; + return raw_width < 6.0? raw_width : 6.0; + +} + FGTACANRecord::FGTACANRecord(void) : channel(""), freq(0) diff --git a/src/Navaids/navrecord.hxx b/src/Navaids/navrecord.hxx index afb60083b..2773629f4 100644 --- a/src/Navaids/navrecord.hxx +++ b/src/Navaids/navrecord.hxx @@ -96,6 +96,13 @@ public: * Retrieve the runway this navaid is associated with (for ILS/LOC/GS) */ FGRunway* runway() const { return mRunway; } + + /** + * return the localizer width, in degrees + * computation is based up ICAO stdandard width at the runway threshold + * see implementation for further details. + */ + double localizerWidth() const; }; class FGTACANRecord : public SGReferenced { From 597df694e352826a974e6341ff708a8cd200747c Mon Sep 17 00:00:00 2001 From: jmt Date: Sat, 27 Mar 2010 17:51:20 +0000 Subject: [PATCH 07/13] GPS/route-manager - notify listeners when current waypoint is changed. --- src/Autopilot/route_mgr.cxx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Autopilot/route_mgr.cxx b/src/Autopilot/route_mgr.cxx index 64263dc25..5996682e1 100644 --- a/src/Autopilot/route_mgr.cxx +++ b/src/Autopilot/route_mgr.cxx @@ -597,6 +597,7 @@ void FGRouteMgr::jumpToIndex(int index) _route->set_current(index); currentWaypointChanged(); + _currentWpt->fireValueChanged(); } void FGRouteMgr::currentWaypointChanged() From 1c7278b474c358e7bac6956ee79d1e8c36346e95 Mon Sep 17 00:00:00 2001 From: fredb Date: Sat, 27 Mar 2010 21:57:14 +0000 Subject: [PATCH 08/13] Initialise member variable --- src/Instrumentation/gps.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index c44cd7110..685754744 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -220,7 +220,8 @@ GPS::GPS ( SGPropertyNode *node) : _num(node->getIntValue("number", 0)), _computeTurnData(false), _anticipateTurn(false), - _inTurn(false) + _inTurn(false), + _desiredCourse(0.0) { string branch = "/instrumentation/" + _name; _gpsNode = fgGetNode(branch.c_str(), _num, true ); From 3270a610f837434dd7b013b17eadf0305280d426 Mon Sep 17 00:00:00 2001 From: jmt Date: Mon, 12 Apr 2010 23:27:29 +0000 Subject: [PATCH 09/13] Support loading plain-text routes, and stop aggressively using the cruise altitude when loading waypoints. --- src/Autopilot/route_mgr.cxx | 50 +++++++++++++++++++++++++++++-------- src/Autopilot/route_mgr.hxx | 3 +++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Autopilot/route_mgr.cxx b/src/Autopilot/route_mgr.cxx index 5996682e1..c9fbe0bfd 100644 --- a/src/Autopilot/route_mgr.cxx +++ b/src/Autopilot/route_mgr.cxx @@ -39,6 +39,7 @@ #include #include +#include #include #include @@ -666,16 +667,23 @@ void FGRouteMgr::saveRoute() void FGRouteMgr::loadRoute() { + // deactivate route first + active->setBoolValue(false); + + SGPropertyNode_ptr routeData(new SGPropertyNode); + SGPath path(_pathNode->getStringValue()); + + SG_LOG(SG_IO, SG_INFO, "going to read flight-plan from:" << path.str()); + try { - // deactivate route first - active->setBoolValue(false); - - SGPropertyNode_ptr routeData(new SGPropertyNode); - SGPath path(_pathNode->getStringValue()); - - SG_LOG(SG_IO, SG_INFO, "going to read flight-plan from:" << path.str()); readProperties(path.str(), routeData); - + } catch (sg_exception& e) { + // if XML parsing fails, the file might be simple textual list of waypoints + loadPlainTextRoute(path); + return; + } + + try { // departure nodes SGPropertyNode* dep = routeData->getChild("departure"); if (!dep) { @@ -704,7 +712,9 @@ void FGRouteMgr::loadRoute() // cruise SGPropertyNode* crs = routeData->getChild("cruise"); if (crs) { - cruise->setDoubleValue(crs->getDoubleValue("speed")); + cruise->setDoubleValue("speed-kts", crs->getDoubleValue("speed-kts")); + cruise->setDoubleValue("mach", crs->getDoubleValue("mach")); + cruise->setDoubleValue("altitude-ft", crs->getDoubleValue("altitude-ft")); } // of cruise data loading // route nodes @@ -735,7 +745,7 @@ void FGRouteMgr::parseRouteWaypoint(SGPropertyNode* aWP) } SGPropertyNode_ptr altProp = aWP->getChild("altitude-ft"); - double altM = cruise->getDoubleValue("altitude-ft") * SG_FEET_TO_METER; + double altM = -9999.0; if (altProp) { altM = altProp->getDoubleValue() * SG_FEET_TO_METER; } @@ -781,6 +791,26 @@ void FGRouteMgr::parseRouteWaypoint(SGPropertyNode* aWP) } } +void FGRouteMgr::loadPlainTextRoute(const SGPath& path) +{ + sg_gzifstream in(path.str().c_str()); + if (!in.is_open()) { + return; + } + + _route->clear(); + while (!in.eof()) { + string line; + getline(in, line, '\n'); + // trim CR from end of line, if found + if (line[line.size() - 1] == '\r') { + line.erase(line.size() - 1, 1); + } + + new_waypoint(line, -1); + } // of line iteration +} + const char* FGRouteMgr::getDepartureICAO() const { if (!_departure) { diff --git a/src/Autopilot/route_mgr.hxx b/src/Autopilot/route_mgr.hxx index a81689248..f3720d09e 100644 --- a/src/Autopilot/route_mgr.hxx +++ b/src/Autopilot/route_mgr.hxx @@ -135,6 +135,9 @@ private: */ bool checkFinished(); + + void loadPlainTextRoute(const SGPath& path); + // tied getters and setters const char* getDepartureICAO() const; const char* getDepartureName() const; From 8ffa65a821d74d915e02bcd90a9b4ddfb987ab18 Mon Sep 17 00:00:00 2001 From: jmt Date: Mon, 12 Apr 2010 23:27:48 +0000 Subject: [PATCH 10/13] Route-file saving. --- src/Autopilot/route_mgr.cxx | 38 +++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/Autopilot/route_mgr.cxx b/src/Autopilot/route_mgr.cxx index c9fbe0bfd..967ca992a 100644 --- a/src/Autopilot/route_mgr.cxx +++ b/src/Autopilot/route_mgr.cxx @@ -658,10 +658,44 @@ void FGRouteMgr::saveRoute() SGPath path(_pathNode->getStringValue()); SG_LOG(SG_IO, SG_INFO, "Saving route to " << path.str()); try { - writeProperties(path.str(), mirror, false, SGPropertyNode::ARCHIVE); + SGPropertyNode_ptr d(new SGPropertyNode); + SGPath path(_pathNode->getStringValue()); + d->setIntValue("version", 1); + + if (_departure) { + d->setStringValue("departure/airport", _departure->ident()); + d->setStringValue("departure/sid", departure->getStringValue("sid")); + d->setStringValue("departure/runway", departure->getStringValue("runway")); + } + + if (_destination) { + d->setStringValue("destination/airport", _destination->ident()); + d->setStringValue("destination/star", destination->getStringValue("star")); + d->setStringValue("destination/transition", destination->getStringValue("transition")); + d->setStringValue("destination/runway", destination->getStringValue("runway")); + } + + // route nodes + SGPropertyNode* routeNode = d->getChild("route", 0, true); + for (int i=0; i<_route->size(); ++i) { + SGPropertyNode* wpNode = routeNode->getChild("wp",i, true); + SGWayPoint wp(_route->get_waypoint(i)); + + wpNode->setStringValue("ident", wp.get_id()); + wpNode->setStringValue("name", wp.get_name()); + SGGeod geod(wp.get_target()); + + wpNode->setDoubleValue("longitude-deg", geod.getLongitudeDeg()); + wpNode->setDoubleValue("latitude-deg", geod.getLatitudeDeg()); + + if (geod.getElevationFt() > -9990.0) { + wpNode->setDoubleValue("altitude-ft", geod.getElevationFt()); + } + } // of waypoint iteration + + writeProperties(path.str(), d, true /* write-all */); } catch (const sg_exception &e) { SG_LOG(SG_IO, SG_WARN, "Error saving route:" << e.getMessage()); - //guiErrorMessage("Error writing autosave.xml: ", e); } } From 0750d8a6d431e0dbe3d6d38f36092c32b6828dc7 Mon Sep 17 00:00:00 2001 From: torsten Date: Wed, 14 Apr 2010 17:26:45 +0000 Subject: [PATCH 11/13] gps warning fix (xxx will be initialized after yyy) --- src/Instrumentation/gps.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Instrumentation/gps.cxx b/src/Instrumentation/gps.cxx index 685754744..1ddb87e08 100644 --- a/src/Instrumentation/gps.cxx +++ b/src/Instrumentation/gps.cxx @@ -213,6 +213,7 @@ void GPS::Config::bind(GPS* aOwner, SGPropertyNode* aCfg) GPS::GPS ( SGPropertyNode *node) : _selectedCourse(0.0), + _desiredCourse(0.0), _dataValid(false), _lastPosValid(false), _mode("init"), @@ -220,8 +221,7 @@ GPS::GPS ( SGPropertyNode *node) : _num(node->getIntValue("number", 0)), _computeTurnData(false), _anticipateTurn(false), - _inTurn(false), - _desiredCourse(0.0) + _inTurn(false) { string branch = "/instrumentation/" + _name; _gpsNode = fgGetNode(branch.c_str(), _num, true ); From 3419b28919e9e537cc6a59fabba342353a1cd261 Mon Sep 17 00:00:00 2001 From: jmt Date: Wed, 21 Apr 2010 16:18:31 +0000 Subject: [PATCH 12/13] MapWidget: fix a crash on uninitialized. --- src/GUI/MapWidget.cxx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/GUI/MapWidget.cxx b/src/GUI/MapWidget.cxx index 0e06b18b9..44b1f7601 100644 --- a/src/GUI/MapWidget.cxx +++ b/src/GUI/MapWidget.cxx @@ -532,18 +532,18 @@ void MapWidget::draw(int dx, int dy) double julianDate = globals->get_time_params()->getJD(); _magVar->update(_projectionCenter, julianDate); - SGGeod topLeft = unproject(SGVec2d(_width/2, _height/2)); - // compute draw range, including a fudge factor for ILSs and other 'long' - // symbols - _drawRangeNm = SGGeodesy::distanceNm(_projectionCenter, topLeft) + 10.0; - bool aircraftUp = _root->getBoolValue("aircraft-heading-up"); if (aircraftUp) { _upHeading = fgGetDouble("/orientation/heading-deg"); } else { _upHeading = 0.0; } - + + SGGeod topLeft = unproject(SGVec2d(_width/2, _height/2)); + // compute draw range, including a fudge factor for ILSs and other 'long' + // symbols + _drawRangeNm = SGGeodesy::distanceNm(_projectionCenter, topLeft) + 10.0; + // drawing operations GLint sx = (int) abox.min[0], sy = (int) abox.min[1]; From 1222e9b09476da09d3a3666a7c0d542b523b579d Mon Sep 17 00:00:00 2001 From: jmt Date: Mon, 26 Apr 2010 16:04:41 +0000 Subject: [PATCH 13/13] Fix a crash if startup fails before the navradio is inited. --- src/Instrumentation/navradio.cxx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Instrumentation/navradio.cxx b/src/Instrumentation/navradio.cxx index b3d621b7d..cdf617530 100644 --- a/src/Instrumentation/navradio.cxx +++ b/src/Instrumentation/navradio.cxx @@ -129,9 +129,14 @@ FGNavRadio::FGNavRadio(SGPropertyNode *node) : // Destructor FGNavRadio::~FGNavRadio() { - gps_course_node->removeChangeListener(this); - nav_slaved_to_gps_node->removeChangeListener(this); - + if (gps_course_node) { + gps_course_node->removeChangeListener(this); + } + + if (nav_slaved_to_gps_node) { + nav_slaved_to_gps_node->removeChangeListener(this); + } + delete term_tbl; delete low_tbl; delete high_tbl;