1
0
Fork 0
flightgear/src/GUI/WaypointList.cxx
James Turner 7317aff22d Fix route-path bugs:
- accurate fly-over / fly-by computations
 - parse additional LevelD XML elements
 - path vector contains curves for turns

Remove dead code.
2014-12-18 23:05:28 +00:00

895 lines
21 KiB
C++

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "WaypointList.hxx"
#include <algorithm>
#include <boost/tuple/tuple.hpp>
#include <plib/puAux.h>
#include <simgear/structure/callback.hxx>
#include <simgear/sg_inlines.h>
#include <Main/globals.hxx>
#include <Main/fg_props.hxx>
#include <Navaids/positioned.hxx>
#include <Navaids/routePath.hxx>
#include <Autopilot/route_mgr.hxx>
// select if the widget grabs keys necessary to fly aircraft from the keyboard,
// or not. See http://code.google.com/p/flightgear-bugs/issues/detail?id=338
// for discussion about why / what is going on.
#define AVOID_FLIGHT_KEYS 1
using namespace flightgear;
enum {
SCROLL_NO = 0,
SCROLL_UP,
SCROLL_DOWN
};
static const double BLINK_TIME = 0.3;
static const int DRAG_START_DISTANCE_PX = 5;
class FlightPlanWaypointModel :
public WaypointList::Model,
public SGPropertyChangeListener
{
public:
FlightPlanWaypointModel(flightgear::FlightPlan* fp) :
_fp(fp)
{
SGPropertyNode* routeEdited = fgGetNode("/autopilot/route-manager/signals/edited", true);
SGPropertyNode* flightplanChanged = fgGetNode("/autopilot/route-manager/signals/flightplan-changed", true);
routeEdited->addChangeListener(this);
flightplanChanged->addChangeListener(this);
}
~FlightPlanWaypointModel()
{
SGPropertyNode* routeEdited = fgGetNode("/autopilot/route-manager/signals/edited", true);
SGPropertyNode* flightplanChanged = fgGetNode("/autopilot/route-manager/signals/flightplan-changed", true);
routeEdited->removeChangeListener(this);
flightplanChanged->removeChangeListener(this);
}
// implement WaypointList::Model
virtual unsigned int numWaypoints() const
{
return _fp->numLegs();
}
virtual int currentWaypoint() const
{
return _fp->currentIndex();
}
virtual flightgear::Waypt* waypointAt(unsigned int index) const
{
if (index >= numWaypoints()) {
return NULL;
}
return _fp->legAtIndex(index)->waypoint();
}
virtual flightgear::FlightPlan* flightplan() const
{
return _fp;
}
virtual void deleteAt(unsigned int index)
{
_fp->deleteIndex(index);
}
virtual void moveWaypointToIndex(unsigned int srcIndex, unsigned int destIndex)
{
SG_LOG(SG_GENERAL, SG_INFO, "moveWaypoint: from " << srcIndex << " to " << destIndex);
if (destIndex > srcIndex) {
--destIndex;
}
int currentWpIndex = currentWaypoint();
WayptRef w = _fp->legAtIndex(srcIndex)->waypoint();
_fp->deleteIndex(srcIndex);
_fp->insertWayptAtIndex(w, destIndex);
if ((signed int) srcIndex == currentWpIndex) {
// current waypoint was moved
_fp->setCurrentIndex(destIndex);
}
}
virtual void setUpdateCallback(SGCallback* cb)
{
_cb = cb;
}
// implement SGPropertyChangeListener
void valueChanged(SGPropertyNode *prop)
{
if (prop->getNameString() == "edited") {
if (_cb) {
(*_cb)();
}
}
if (prop->getNameString() == "flightplan-changed") {
_fp =
static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"))->flightPlan();
}
}
private:
flightgear::FlightPlan* _fp;
SGCallback* _cb;
};
//////////////////////////////////////////////////////////////////////////////
static void drawClippedString(puFont& font, const char* s, int x, int y, int maxWidth)
{
int fullWidth = font.getStringWidth(s);
if (fullWidth <= maxWidth) { // common case, easy and efficent
font.drawString(s, x, y);
return;
}
std::string buf(s);
int len = buf.size();
do {
buf.resize(--len);
fullWidth = font.getStringWidth(buf.c_str());
} while (fullWidth > maxWidth);
font.drawString(buf.c_str(), x, y);
}
//////////////////////////////////////////////////////////////////////////////
WaypointList::WaypointList(int x, int y, int width, int height) :
puFrame(x, y, width, height),
GUI_ID(FGCLASS_WAYPOINTLIST),
_scrollPx(0),
_dragging(false),
_dragScroll(SCROLL_NO),
_showLatLon(false),
_model(NULL),
_updateCallback(NULL),
_scrollCallback(NULL),
_blink(false)
{
// pretend to be a list, so fgPopup doesn't mess with our mouse events
type |= PUCLASS_LIST;
flightgear::FlightPlan* fp =
static_cast<FGRouteMgr*>(globals->get_subsystem("route-manager"))->flightPlan();
setModel(new FlightPlanWaypointModel(fp));
setSize(width, height);
setValue(-1);
_blinkTimer.stamp();
}
WaypointList::~WaypointList()
{
delete _model;
delete _updateCallback;
delete _scrollCallback;
}
void WaypointList::setUpdateCallback(SGCallback* cb)
{
_updateCallback = cb;
}
void WaypointList::setScrollCallback(SGCallback* cb)
{
_scrollCallback = cb;
}
void WaypointList::setSize(int width, int height)
{
double scrollP = getVScrollPercent();
_heightPx = height;
puFrame::setSize(width, height);
if (wantsVScroll()) {
setVScrollPercent(scrollP);
} else {
_scrollPx = 0;
}
}
int WaypointList::checkHit ( int button, int updown, int x, int y )
{
if ( isHit( x, y ) || ( puActiveWidget () == this ) )
{
doHit ( button, updown, x, y ) ;
return TRUE ;
}
return FALSE ;
}
void WaypointList::doHit( int button, int updown, int x, int y )
{
puFrame::doHit(button, updown, x, y);
if (updown == PU_DRAG) {
handleDrag(x, y);
return;
}
if (button != active_mouse_button) {
return;
}
if (updown == PU_UP) {
puDeactivateWidget();
if (_dragging) {
doDrop(x, y);
return;
}
} else if (updown == PU_DOWN) {
puSetActiveWidget(this, x, y);
_mouseDownX = x;
_mouseDownY = y;
return;
}
// update selection
int row = rowForY(y - abox.min[1]);
if (row >= (int) _model->numWaypoints()) {
row = -1; // 'no selection'
}
if (row == getSelected()) {
_showLatLon = !_showLatLon;
puPostRefresh();
return;
}
setSelected(row);
}
void WaypointList::handleDrag(int x, int y)
{
if (!_dragging) {
// don't start drags immediately, require a certain mouse movement first
int manhattanLength = abs(x - _mouseDownX) + abs(y - _mouseDownY);
if (manhattanLength < DRAG_START_DISTANCE_PX) {
return;
}
_dragSourceRow = rowForY(y - abox.min[1]);
Waypt* wp = _model->waypointAt(_dragSourceRow);
if (!wp || wp->flag(WPT_GENERATED)) {
return; // don't allow generated points to be dragged
}
_dragging = true;
_dragScroll = SCROLL_NO;
}
if (y < abox.min[1]) {
if (_dragScroll != SCROLL_DOWN) {
_dragScroll = SCROLL_DOWN;
_dragScrollTime.stamp();
}
} else if (y > abox.max[1]) {
if (_dragScroll != SCROLL_UP) {
_dragScroll = SCROLL_UP;
_dragScrollTime.stamp();
}
} else {
_dragScroll = SCROLL_NO;
_dragTargetRow = rowForY(y - abox.min[1] - (rowHeightPx() / 2));
}
}
void WaypointList::doDrop(int x, int y)
{
_dragging = false;
puDeactivateWidget();
SG_LOG(SG_GENERAL, SG_INFO, "doDrop");
if ((y < abox.min[1]) || (y >= abox.max[1])) {
SG_LOG(SG_GENERAL, SG_INFO, "y out of bounds:" << y);
return;
}
if (_dragSourceRow == _dragTargetRow) {
SG_LOG(SG_GENERAL, SG_INFO, "source and target row match");
return;
}
_model->moveWaypointToIndex(_dragSourceRow, _dragTargetRow);
// keep row indexes linged up when moving an item down the list
if (_dragSourceRow < _dragTargetRow) {
--_dragTargetRow;
}
setSelected(_dragTargetRow);
}
void WaypointList::invokeDownCallback(void)
{
_dragging = false;
_dragScroll = SCROLL_NO;
SG_LOG(SG_GENERAL, SG_INFO, "cancel drag");
}
int WaypointList::rowForY(int y) const
{
if (!_model) {
return -1;
}
// flip y to increase down, not up (as rows do)
int flipY = _heightPx - y;
int row = (flipY + _scrollPx) / rowHeightPx();
return row;
}
void WaypointList::draw( int dx, int dy )
{
puFrame::draw(dx, dy);
if (!_model) {
return;
}
if (_dragScroll != SCROLL_NO) {
doDragScroll();
}
double dt = (SGTimeStamp::now() - _blinkTimer).toSecs();
if (dt > BLINK_TIME) {
_blinkTimer.stamp();
_blink = !_blink;
}
glEnable(GL_SCISSOR_TEST);
GLint sx = (int) abox.min[0],
sy = abox.min[1];
GLsizei w = (GLsizei) abox.max[0] - abox.min[0],
h = _heightPx;
sx += border_thickness;
sy += border_thickness;
w -= 2 * border_thickness;
h -= 2 * border_thickness;
glScissor(sx + dx, sy + dy, w, h);
int row = firstVisibleRow(),
final = lastVisibleRow(),
rowHeight = rowHeightPx(),
y = rowHeight;
y -= (_scrollPx % rowHeight); // partially draw the first row
_arrowWidth = legendFont.getStringWidth(">");
RoutePath path(_model->flightplan());
for ( ; row <= final; ++row, y += rowHeight) {
drawRow(dx, dy, row, y, path);
} // of row drawing iteration
glDisable(GL_SCISSOR_TEST);
if (_dragging) {
// draw the insert marker after the rows
int insertY = (_dragTargetRow * rowHeight) - _scrollPx;
SG_CLAMP_RANGE(insertY, 0, std::min(_heightPx, totalHeightPx()));
glColor4f(1.0f, 0.5f, 0.0f, 0.8f);
glLineWidth(3.0f);
glBegin(GL_LINES);
glVertex2f(dx + abox.min[0], dy + abox.max[1] - insertY);
glVertex2f(dx + abox.max[0], dy + abox.max[1] - insertY);
glEnd();
}
}
void WaypointList::drawRow(int dx, int dy, int rowIndex, int y,
const RoutePath& path)
{
flightgear::Waypt* wp(_model->waypointAt(rowIndex));
bool isSelected = (rowIndex == getSelected());
bool isCurrent = (rowIndex == _model->currentWaypoint());
bool isDragSource = (_dragging && (rowIndex == _dragSourceRow));
puBox bkgBox = abox;
bkgBox.min[1] = abox.max[1] - y;
bkgBox.max[1] = bkgBox.min[1] + rowHeightPx();
puColour col;
puFont* f = &legendFont;
bool drawBox = false;
if (wp->flag(WPT_MISS)) {
drawBox = true;
puSetColor(col, 1.0, 0.0, 0.0, 0.3); // red
} else if (wp->flag(WPT_ARRIVAL) || wp->flag(WPT_DEPARTURE)) {
drawBox = true;
puSetColor(col, 0.0, 0.0, 0.0, 0.3);
} else if (wp->flag(WPT_APPROACH)) {
drawBox = true;
puSetColor(col, 0.0, 0.0, 0.1, 0.3);
}
if (isDragSource) {
// draw later, on *top* of text string
} else if (isSelected) { // -PLAIN means selected, apparently
bkgBox.draw(dx, dy, -PUSTYLE_PLAIN, colour, false, 0);
} else if (drawBox) {
bkgBox.draw(dx, dy, PUSTYLE_PLAIN, &col, false, 0);
}
if (isCurrent) {
glColor4f (1.0, 0.5, 0.0, 1.0) ;
} else {
glColor4fv ( colour [ PUCOL_LEGEND ] ) ;
}
int xx = dx + abox.min[0] + PUSTR_LGAP;
int yy = dy + abox.max[1] - y ;
yy += 4; // center text in row height
if (isCurrent) {
f->drawString(">", xx, yy);
}
int x = xx;
x += _arrowWidth + PUSTR_LGAP;
// row textual data
char buffer[128];
int count = ::snprintf(buffer, 128, "%03d %-5s", rowIndex, wp->ident().c_str());
FGPositioned* src = wp->source();
if (src && !src->name().empty() && (src->name() != wp->ident())) {
// append name if present, and different to id
::snprintf(buffer + count, 128 - count, " (%s)", src->name().c_str());
}
drawClippedString(legendFont, buffer, x, yy, 300);
x += 300 + PUSTR_LGAP;
if (_showLatLon) {
// only show for non-dynamic waypoints
if (!wp->flag(WPT_DYNAMIC)) {
SGGeod p(wp->position());
char ns = (p.getLatitudeDeg() > 0.0) ? 'N' : 'S';
char ew = (p.getLongitudeDeg() > 0.0) ? 'E' : 'W';
::snprintf(buffer, 128 - count, "%4.2f%c %4.2f%c",
fabs(p.getLongitudeDeg()), ew, fabs(p.getLatitudeDeg()), ns);
} else {
buffer[0] = 0;
}
} else if (rowIndex > 0) {
double courseDeg = path.trackForIndex(rowIndex);
double distanceM = path.distanceForIndex(rowIndex);
::snprintf(buffer, 128 - count, "%03.0f %5.1fnm",
courseDeg, distanceM * SG_METER_TO_NM);
}
f->drawString(buffer, x, yy);
x += 100 + PUSTR_LGAP;
if (wp->altitudeRestriction() != RESTRICT_NONE) {
char aboveAtBelow = ' ';
if (wp->altitudeRestriction() == RESTRICT_ABOVE) {
aboveAtBelow = 'A';
} else if (wp->altitudeRestriction() == RESTRICT_BELOW) {
aboveAtBelow = 'B';
}
int altHundredFt = (wp->altitudeFt() + 50) / 100; // round to nearest 100ft
if (altHundredFt < 100) {
count = ::snprintf(buffer, 128, "%d'%c", altHundredFt * 100, aboveAtBelow);
} else { // display as a flight-level
count = ::snprintf(buffer, 128, "FL%d%c", altHundredFt, aboveAtBelow);
}
f->drawString(buffer, x, yy);
} // of valid wp altitude
x += 60 + PUSTR_LGAP;
if (wp->speedRestriction() == SPEED_RESTRICT_MACH) {
count = ::snprintf(buffer, 126, "%03.2fM", wp->speedMach());
f->drawString(buffer, x, yy);
} else if (wp->speedRestriction() != RESTRICT_NONE) {
count = ::snprintf(buffer, 126, "%dKts", (int) wp->speedKts());
f->drawString(buffer, x, yy);
}
if (isDragSource) {
puSetColor(col, 1.0, 0.5, 0.0, 0.5);
bkgBox.draw(dx, dy, PUSTYLE_PLAIN, &col, false, 0);
}
}
const double SCROLL_PX_SEC = 200.0;
void WaypointList::doDragScroll()
{
double dt = (SGTimeStamp::now() - _dragScrollTime).toSecs();
_dragScrollTime.stamp();
int deltaPx = (int)(dt * SCROLL_PX_SEC);
if (_dragScroll == SCROLL_UP) {
_scrollPx = _scrollPx - deltaPx;
SG_CLAMP_RANGE(_scrollPx, 0, scrollRangePx());
_dragTargetRow = firstVisibleRow();
} else {
_scrollPx = _scrollPx + deltaPx;
SG_CLAMP_RANGE(_scrollPx, 0, scrollRangePx());
_dragTargetRow = lastFullyVisibleRow() + 1;
}
if (_scrollCallback) {
(*_scrollCallback)();
}
}
int WaypointList::getSelected()
{
return getIntegerValue();
}
void WaypointList::setSelected(int rowIndex)
{
if (rowIndex == getSelected()) {
return;
}
setValue(rowIndex);
invokeCallback();
if (rowIndex == -1) {
return;
}
ensureRowVisible(rowIndex);
}
void WaypointList::ensureRowVisible(int rowIndex)
{
if ((rowIndex >= firstFullyVisibleRow()) && (rowIndex <= lastFullyVisibleRow())) {
return; // already visible, fine
}
// ideal position would place the desired row in the middle of the
// visible section - hence subtract half the visible height.
int targetScrollPx = (rowIndex * rowHeightPx()) - (_heightPx / 2);
// clamp the scroll value to something valid
SG_CLAMP_RANGE(targetScrollPx, 0, scrollRangePx());
_scrollPx = targetScrollPx;
puPostRefresh();
if (_scrollCallback) { // keep scroll observers in sync
(*_scrollCallback)();
}
}
unsigned int WaypointList::numWaypoints() const
{
if (!_model) {
return 0;
}
return _model->numWaypoints();
}
bool WaypointList::wantsVScroll() const
{
return totalHeightPx() > _heightPx;
}
float WaypointList::getVScrollPercent() const
{
float scrollRange = scrollRangePx();
if (scrollRange < 1.0f) {
return 0.0;
}
return _scrollPx / scrollRange;
}
float WaypointList::getVScrollThumbPercent() const
{
return _heightPx / (float) totalHeightPx();
}
void WaypointList::setVScrollPercent(float perc)
{
float scrollRange = scrollRangePx();
_scrollPx = (int)(scrollRange * perc);
}
int WaypointList::firstVisibleRow() const
{
return _scrollPx / rowHeightPx();
}
int WaypointList::firstFullyVisibleRow() const
{
int rh = rowHeightPx();
return (_scrollPx + rh - 1) / rh;
}
int WaypointList::numVisibleRows() const
{
int rh = rowHeightPx();
int topOffset = _scrollPx % rh; // pixels of first visible row
return (_heightPx - topOffset + rh - 1) / rh;
}
int WaypointList::numFullyVisibleRows() const
{
int rh = rowHeightPx();
int topOffset = _scrollPx % rh; // pixels of first visible row
return (_heightPx - topOffset) / rh;
}
int WaypointList::rowHeightPx() const
{
return legendFont.getStringHeight() + PUSTR_BGAP;
}
int WaypointList::scrollRangePx() const
{
return std::max(0, totalHeightPx() - _heightPx);
}
int WaypointList::totalHeightPx() const
{
if (!_model) {
return 0;
}
return (int) _model->numWaypoints() * rowHeightPx();
}
int WaypointList::lastFullyVisibleRow() const
{
int row = firstFullyVisibleRow() + numFullyVisibleRows();
return std::min(row, (int) _model->numWaypoints() - 1);
}
int WaypointList::lastVisibleRow() const
{
int row = firstVisibleRow() + numVisibleRows();
return std::min(row, (int) _model->numWaypoints() - 1);
}
void WaypointList::setModel(Model* model)
{
if (_model) {
delete _model;
}
_model = model;
_model->setUpdateCallback(make_callback(this, &WaypointList::modelUpdateCallback));
puPostRefresh();
}
int WaypointList::checkKey (int key, int updown )
{
if ((updown == PU_UP) || !isVisible () || !isActive () || (window != puGetWindow())) {
return FALSE ;
}
#ifdef AVOID_FLIGHT_KEYS
return FALSE;
#endif
switch (key)
{
case PU_KEY_HOME:
setSelected(0);
break;
case PU_KEY_END:
setSelected(_model->numWaypoints() - 1);
break ;
case PU_KEY_UP :
case PU_KEY_PAGE_UP :
if (getSelected() >= 0) {
setSelected(getSelected() - 1);
}
break ;
case PU_KEY_DOWN :
case PU_KEY_PAGE_DOWN : {
int newSel = getSelected() + 1;
if (newSel >= (int) _model->numWaypoints()) {
setSelected(-1);
} else {
setSelected(newSel);
}
break ;
}
case '-':
if (getSelected() >= 0) {
Waypt* wp = _model->waypointAt(getSelected());
if (wp->flag(WPT_GENERATED)) {
break;
}
if (wp->altitudeRestriction() != RESTRICT_NONE) {
int curAlt = (static_cast<int>(wp->altitudeFt()) + 50) / 100;
if (curAlt <= 0) {
wp->setAltitude(0, RESTRICT_NONE);
} else {
wp->setAltitude((curAlt - 10) * 100, wp->altitudeRestriction());
}
}
}
break;
case '=':
if (getSelected() >= 0) {
flightgear::Waypt* wp = _model->waypointAt(getSelected());
if (wp->flag(WPT_GENERATED)) {
break;
}
if (wp->altitudeRestriction() == RESTRICT_NONE) {
wp->setAltitude(1000, RESTRICT_AT);
} else {
int curAlt = (static_cast<int>(wp->altitudeFt()) + 50) / 100;
wp->setAltitude((curAlt + 10) * 100, wp->altitudeRestriction());
}
}
break;
case 0x7f: // delete
if (getSelected() >= 0) {
int index = getSelected();
Waypt* wp = _model->waypointAt(getSelected());
if (wp->flag(WPT_GENERATED)) {
break;
}
_model->deleteAt(index);
setSelected(index - 1);
}
break;
default :
return FALSE;
}
return TRUE ;
}
void WaypointList::modelUpdateCallback()
{
// local stuff
if (_updateCallback) {
(*_updateCallback)();
}
}
//////////////////////////////////////////////////////////////////////////////
static void handle_scrollbar(puObject* scrollbar)
{
ScrolledWaypointList* self = (ScrolledWaypointList*)scrollbar->getUserData();
self->setScrollPercent(scrollbar->getFloatValue());
}
static void waypointListCb(puObject* wpl)
{
ScrolledWaypointList* self = (ScrolledWaypointList*)wpl->getUserData();
self->setValue(wpl->getIntegerValue());
self->invokeCallback();
}
ScrolledWaypointList::ScrolledWaypointList(int x, int y, int width, int height) :
puGroup(x,y),
_scrollWidth(16)
{
// ensure our type is compound, so fgPopup::applySize doesn't descend into
// us, and try to cast our children's user-data to GUIInfo*.
type |= PUCLASS_LIST;
init(width, height);
}
void ScrolledWaypointList::setValue(float v)
{
puGroup::setValue(v);
_list->setValue(v);
}
void ScrolledWaypointList::setValue(int v)
{
puGroup::setValue(v);
_list->setValue(v);
}
void ScrolledWaypointList::init(int w, int h)
{
_list = new WaypointList(0, 0, w, h);
_list->setUpdateCallback(make_callback(this, &ScrolledWaypointList::modelUpdated));
_hasVScroll = _list->wantsVScroll();
_list->setUserData(this);
_list->setCallback(waypointListCb);
_list->setScrollCallback(make_callback(this, &ScrolledWaypointList::updateScroll));
_scrollbar = new puaScrollBar(w - _scrollWidth, 0, h,
1 /*arrow*/, 1 /* vertical */, _scrollWidth);
_scrollbar->setMinValue(0.0);
_scrollbar->setMaxValue(1.0);
_scrollbar->setUserData(this);
_scrollbar->setCallback(handle_scrollbar);
close(); // close the group
setSize(w, h);
}
void ScrolledWaypointList::modelUpdated()
{
int w, h;
getSize(&w, &h);
updateWantsScroll(w, h);
}
void ScrolledWaypointList::setScrollPercent(float v)
{
// slider's min is the bottom, so invert the value
_list->setVScrollPercent(1.0f - v);
}
void ScrolledWaypointList::setSize(int w, int h)
{
updateWantsScroll(w, h);
puGroup::setSize(w, h);
}
void ScrolledWaypointList::updateWantsScroll(int w, int h)
{
_hasVScroll = _list->wantsVScroll();
if (_hasVScroll) {
_scrollbar->reveal();
_scrollbar->setPosition(w - _scrollWidth, 0);
_scrollbar->setSize(_scrollWidth, h);
_list->setSize(w - _scrollWidth, h);
updateScroll();
} else {
_scrollbar->hide();
_list->setSize(w, h);
}
}
void ScrolledWaypointList::updateScroll()
{
// _scrollbar->setMaxValue(_list->numWaypoints());
_scrollbar->setValue(1.0f - _list->getVScrollPercent());
_scrollbar->setSliderFraction(_list->getVScrollThumbPercent());
}