commit 928e259d0c15b1cc78e28627c3f7ff94d57b52b7 Author: Martin Tůma Date: Mon Oct 5 01:43:48 2015 +0200 Initial commit diff --git a/gpxsee.pro b/gpxsee.pro new file mode 100644 index 00000000..804443af --- /dev/null +++ b/gpxsee.pro @@ -0,0 +1,36 @@ +TARGET = gpxsee +QT += core \ + gui +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets +greaterThan(QT_MAJOR_VERSION, 4): QT += printsupport +HEADERS += src/config.h \ + src/icons.h \ + src/gui.h \ + src/gpx.h \ + src/graph.h \ + src/track.h \ + src/parser.h \ + src/poi.h \ + src/rtree.h \ + src/ll.h \ + src/axisitem.h \ + src/poiitem.h \ + src/colorshop.h \ + src/keys.h \ + src/slideritem.h \ + src/markeritem.h +SOURCES += src/main.cpp \ + src/gui.cpp \ + src/gpx.cpp \ + src/graph.cpp \ + src/track.cpp \ + src/parser.cpp \ + src/poi.cpp \ + src/ll.cpp \ + src/axisitem.cpp \ + src/poiitem.cpp \ + src/colorshop.cpp \ + src/slideritem.cpp \ + src/markeritem.cpp +RESOURCES += gpxsee.qrc +TRANSLATIONS = lang/gpxsee_cs.ts diff --git a/gpxsee.qrc b/gpxsee.qrc new file mode 100644 index 00000000..d9ad1373 --- /dev/null +++ b/gpxsee.qrc @@ -0,0 +1,14 @@ + + + icons/dialog-close.png + icons/document-open.png + icons/document-save-as.png + icons/document-save.png + icons/flag.png + icons/gpxsee.png + + + + lang/gpxsee_cs.qm + + diff --git a/icons/dialog-close.png b/icons/dialog-close.png new file mode 100644 index 00000000..5492295e Binary files /dev/null and b/icons/dialog-close.png differ diff --git a/icons/document-open.png b/icons/document-open.png new file mode 100644 index 00000000..bc8cface Binary files /dev/null and b/icons/document-open.png differ diff --git a/icons/document-save-as.png b/icons/document-save-as.png new file mode 100644 index 00000000..0ecb79e8 Binary files /dev/null and b/icons/document-save-as.png differ diff --git a/icons/document-save.png b/icons/document-save.png new file mode 100644 index 00000000..a81e70d4 Binary files /dev/null and b/icons/document-save.png differ diff --git a/icons/flag.png b/icons/flag.png new file mode 100644 index 00000000..3d93e125 Binary files /dev/null and b/icons/flag.png differ diff --git a/icons/gpxsee.dia b/icons/gpxsee.dia new file mode 100644 index 00000000..34a91b52 Binary files /dev/null and b/icons/gpxsee.dia differ diff --git a/icons/gpxsee.png b/icons/gpxsee.png new file mode 100644 index 00000000..5569d607 Binary files /dev/null and b/icons/gpxsee.png differ diff --git a/lang/gpxsee_cs.ts b/lang/gpxsee_cs.ts new file mode 100644 index 00000000..80711f81 --- /dev/null +++ b/lang/gpxsee_cs.ts @@ -0,0 +1,165 @@ + + + + + GUI + + + Exit + Konec + + + + About Qt + O Qt + + + + GPXSee is distributed under the terms of the GNU General Public License version 3. For more info about GPXSee visit the project homepage at + Program GPXSee je distribuován pod podmínkami licence GNU General Public License verze 3. Pro více informací navštivte stránky programu na adrese + + + + Open file + Otevřít soubor + + + + Save as + Uložit jako + + + + Open POI file + Otevřít POI soubor + + + + Open + Otevřít + + + + Save + Uložit + + + + Close + Zavřít + + + + Load file + Nahrát soubor + + + + Show + Zobrazit + + + + + File + Soubor + + + + + POI + POI + + + + Help + Nápověda + + + + + Distance [km] + Vzdálenost [km] + + + + Elevation [m.a.s.l.] + Výška [m.n.m.] + + + + Speed [km/h] + Rychlost [km/h] + + + + Elevation + Výška + + + + Speed + Rychlost + + + + + About GPXSee + O aplikaci GPXSee + + + + GPX viewer and analyzer + Prohlížeč a analyzátor GPX + + + + + Error + Chyba + + + + Error loading GPX file: +%1 + Soubor GPX nelze otevřít: +%1 + + + + Error loading POI file: +%1 + Soubor POI nelze otevřít: +%1 + + + + QObject + + + Not a GPX file. + Neplatný GPX soubor. + + + + %1 +Line %2 + %1 +Rádka %2 + + + + Invalid latitude +Line %1 + Neplatná zeměpisná šířka +Rádka %1 + + + + Invalid longitude +Line %1 + Neplatná zeměpisná délka +Rádka %1 + + + diff --git a/src/axisitem.cpp b/src/axisitem.cpp new file mode 100644 index 00000000..d8842947 --- /dev/null +++ b/src/axisitem.cpp @@ -0,0 +1,204 @@ +#include +#include +#include "axisitem.h" + +#include + +#define TICK 6 +#define PADDING 6 +#define XTICKS 15 +#define YTICKS 10 +#define FONT_FAMILY "Arial" +#define FONT_SIZE 12 + + +struct Label { + double min; + double max; + double d; +}; + +static double niceNum(double x, int round) +{ + int expv; + double f; + double nf; + + expv = floor(log10(x)); + f = x / pow(10.0, expv); + + if (round) { + if (f < 1.5) + nf = 1.0; + else if (f < 3.0) + nf = 2.0; + else if (f < 7.0) + nf = 5.0; + else + nf = 10.0; + } else { + if (f <= 1.0) + nf = 1.; + else if (f <= 2.0) + nf = 2.0; + else if (f <= 5.0) + nf = 5.0; + else + nf = 10.0; + } + + return nf * pow(10.0, expv); +} + +static struct Label label(double min, double max, int ticks) +{ + double range; + struct Label l; + + range = niceNum(max - min, 0); + l.d = niceNum(range / ticks, 1); + l.min = ceil(min / l.d) * l.d; + l.max = floor(max / l.d) * l.d; + + return l; +} + + +AxisItem::AxisItem(Type type) +{ + _type = type; + _size = 0; +} + +void AxisItem::setRange(const QPointF &range) +{ + _range = range; + updateBoundingRect(); + prepareGeometryChange(); +} + +void AxisItem::setSize(qreal size) +{ + _size = size; + updateBoundingRect(); + prepareGeometryChange(); +} + +void AxisItem::setLabel(const QString& label) +{ + _label = label; + updateBoundingRect(); + prepareGeometryChange(); +} + +void AxisItem::updateBoundingRect() +{ + QFont font; + font.setPixelSize(FONT_SIZE); + font.setFamily(FONT_FAMILY); + QFontMetrics fm(font); + QRect ss, es, ls; + struct Label l; + + + l = label(_range.x(), _range.y(), (_type == X) ? XTICKS : YTICKS); + es = fm.tightBoundingRect(QString::number(l.max)); + ss = fm.tightBoundingRect(QString::number(l.min)); + ls = fm.tightBoundingRect(_label); + + if (_type == X) { + _boundingRect = QRectF(-ss.width()/2, -TICK/2, + _size + es.width()/2 + ss.width()/2, + ls.height() + es.height() - fm.descent() + TICK + 2*PADDING); + } else { + _boundingRect = QRectF(-(ls.height() + es.width() + 2*PADDING + - fm.descent() + TICK/2), -(_size + es.height()/2 + + fm.descent()), ls.height() -fm.descent() + es.width() + 2*PADDING + + TICK, _size + es.height()/2 + fm.descent() + ss.height()/2); + } +} + +void AxisItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + QFont font; + font.setPixelSize(FONT_SIZE); + font.setFamily(FONT_FAMILY); + painter->setFont(font); + QFontMetrics fm(font); + QRect ts, ls; + struct Label l; + qreal range = _range.y() - _range.x(); + qreal val; + + + ls = fm.tightBoundingRect(_label); + + if (_type == X) { + painter->drawLine(0, 0, _size, 0); + + l = label(_range.x(), _range.y(), XTICKS); + for (int i = 0; i < ((l.max - l.min) / l.d) + 1; i++) { + val = l.min + i * l.d; + QString str = QString::number(val); + + painter->drawLine((_size/range) * (val - _range.x()), TICK/2, + (_size/range) * (val - _range.x()), -TICK/2); + ts = fm.tightBoundingRect(str); + painter->drawText(((_size/range) * (val - _range.x())) + - (ts.width()/2), ts.height() + TICK/2 + PADDING, str); + } + + painter->drawText(_size/2 - ls.width()/2, ls.height() + ts.height() + - 2*fm.descent() + TICK/2 + 2*PADDING, _label); + } else { + painter->drawLine(0, 0, 0, -_size); + + l = label(_range.x(), _range.y(), YTICKS); + for (int i = 0; i < ((l.max - l.min) / l.d) + 1; i++) { + val = l.min + i * l.d; + QString str = QString::number(val); + + painter->drawLine(TICK/2, -((_size/range) * (val - _range.x())), + -TICK/2, -((_size/range) * (val - _range.x()))); + ts = fm.tightBoundingRect(str); + painter->drawText(-(ts.width() + PADDING + TICK/2), -((_size/range) + * (val - _range.x())) + (ts.height()/2), str); + } + + painter->rotate(-90); + painter->drawText(_size/2 - ls.width()/2, -(ts.width() + + 2*PADDING + TICK/2), _label); + painter->rotate(90); + } + +/* + painter->setPen(Qt::red); + painter->drawRect(boundingRect()); +*/ +} + +QSizeF AxisItem::margin() +{ + QFont font; + font.setPixelSize(FONT_SIZE); + QFontMetrics fm(font); + QRect ss, es, ls; + struct Label l; + + + l = label(_range.x(), _range.y(), (_type == X) ? XTICKS : YTICKS); + es = fm.tightBoundingRect(QString::number(l.max)); + ss = fm.tightBoundingRect(QString::number(l.min)); + ls = fm.tightBoundingRect(_label); + + if (_type == X) { + return QSizeF(es.width()/2, + ls.height() + es.height() - fm.descent() + TICK/2 + 2*PADDING); + } else { + return QSizeF(ls.height() -fm.descent() + es.width() + 2*PADDING + + TICK/2, es.height()/2 + fm.descent()); + } +} diff --git a/src/axisitem.h b/src/axisitem.h new file mode 100644 index 00000000..31da4058 --- /dev/null +++ b/src/axisitem.h @@ -0,0 +1,33 @@ +#ifndef AXISITEM_H +#define AXISITEM_H + +#include + +class AxisItem : public QGraphicsItem +{ +public: + enum Type {X, Y}; + + AxisItem(Type type); + + QRectF boundingRect() const {return _boundingRect;} + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget); + + void setRange(const QPointF &range); + void setSize(qreal size); + void setLabel(const QString& label); + + QSizeF margin(); + +private: + void updateBoundingRect(); + + Type _type; + QPointF _range; + qreal _size; + QString _label; + QRectF _boundingRect; +}; + +#endif // AXISITEM_H diff --git a/src/colorshop.cpp b/src/colorshop.cpp new file mode 100644 index 00000000..f5143dbd --- /dev/null +++ b/src/colorshop.cpp @@ -0,0 +1,62 @@ +#include "colorshop.h" + +#define HUE_INIT 0.1f +#define HUE_INCREMENT 0.62f +#define SATURATION 0.99f +#define VALUE 0.99f + + +static unsigned hsv2rgb(float h, float s, float v) +{ + unsigned hi; + float r = 0, g = 0, b = 0, p, q, t, f; + + hi = (unsigned)(h * 6.0f); + f = h * 6.0f - hi; + p = v * (1.0f - s); + q = v * (1.0f - f * s); + t = v * (1.0f - (1.0f - f) * s); + + switch (hi) { + case 0: + r = v; g = t; b = p; + break; + case 1: + r = q; g = v; b = p; + break; + case 2: + r = p; g = v; b = t; + break; + case 3: + r = p; g = q; b = v; + break; + case 4: + r = t; g = p; b = v; + break; + case 5: + r = v; g = p; b = q; + break; + } + + return ((unsigned)(r * 256) << 16) + + ((unsigned)(g * 256) << 8) + + (unsigned)(b * 256); +} + +ColorShop::ColorShop() +{ + _hueState = HUE_INIT; +} + +QColor ColorShop::color() +{ + _hueState += HUE_INCREMENT; + _hueState -= (int) _hueState; + + return QColor(hsv2rgb(_hueState, SATURATION, VALUE)); +} + +void ColorShop::reset() +{ + _hueState = HUE_INIT; +} diff --git a/src/colorshop.h b/src/colorshop.h new file mode 100644 index 00000000..b7421619 --- /dev/null +++ b/src/colorshop.h @@ -0,0 +1,17 @@ +#ifndef COLORSHOP_H +#define COLORSHOP_H + +#include + +class ColorShop +{ +public: + ColorShop(); + QColor color(); + void reset(); + +private: + float _hueState; +}; + +#endif // COLORSHOP_H diff --git a/src/config.h b/src/config.h new file mode 100644 index 00000000..c5aecb22 --- /dev/null +++ b/src/config.h @@ -0,0 +1,8 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define APP_NAME "GPXSee" +#define APP_HOMEPAGE "http://tumic.wz.cz/gpxsee" +#define APP_VERSION "0.1" + +#endif /* CONFIG_H */ diff --git a/src/gpx.cpp b/src/gpx.cpp new file mode 100644 index 00000000..f7253f2a --- /dev/null +++ b/src/gpx.cpp @@ -0,0 +1,82 @@ +#include +#include +#include "ll.h" +#include "gpx.h" + + +#define ALPHA_E 0.9 +#define ALPHA_S 0.1 + +bool GPX::loadFile(const QString &fileName) +{ + QFile file(fileName); + bool ret; + + _data.clear(); + _error.clear(); + + if (!file.open(QFile::ReadOnly | QFile::Text)) { + _error = qPrintable(file.errorString()); + return false; + } + + if (!(ret = _parser.loadFile(&file, _data))) + _error = _parser.errorString(); + file.close(); + + return ret; +} + +QVector GPX::elevationGraph() const +{ + QVector graph; + qreal dist = 0, ds, dh, acc; + + if (!_data.size()) + return graph; + + graph.append(QPointF(0, _data.at(0).elevation)); + for (int i = 1; i < _data.size(); i++) { + ds = llDistance(_data.at(i).coordinates, _data.at(i-1).coordinates); + dh = _data.at(i).elevation; + dist += ds; + acc = (i == 1) ? dh : (ALPHA_E * dh) + (1.0 - ALPHA_E) * acc; + graph.append(QPointF(dist, acc)); + } + + return graph; +} + +QVector GPX::speedGraph() const +{ + QVector graph; + qreal dist = 0, v, ds, dt, acc; + + if (!_data.size()) + return graph; + + graph.append(QPointF(0, 0)); + for (int i = 1; i < _data.size(); i++) { + ds = llDistance(_data.at(i).coordinates, _data.at(i-1).coordinates); + dt = _data.at(i-1).timestamp.msecsTo(_data.at(i).timestamp) / 1000.0; + dist += ds; + v = ds / dt; + acc = (i == 1) ? v : (ALPHA_S * v) + (1.0 - ALPHA_S) * acc; + graph.append(QPointF(dist, acc)); + } + + return graph; +} + +QVector GPX::track() const +{ + QVector track; + QPointF p; + + for (int i = 0; i < _data.size(); i++) { + ll2mercator(_data.at(i).coordinates, p); + track.append(p); + } + + return track; +} diff --git a/src/gpx.h b/src/gpx.h new file mode 100644 index 00000000..582d2014 --- /dev/null +++ b/src/gpx.h @@ -0,0 +1,24 @@ +#ifndef GPX_H +#define GPX_H + +#include +#include +#include +#include "parser.h" + +class GPX +{ +public: + bool loadFile(const QString &fileName); + const QString &errorString() const {return _error;} + QVector elevationGraph() const; + QVector speedGraph() const; + QVector track() const; + +private: + Parser _parser; + QVector _data; + QString _error; +}; + +#endif // GPX_H diff --git a/src/graph.cpp b/src/graph.cpp new file mode 100644 index 00000000..dfb0fc4b --- /dev/null +++ b/src/graph.cpp @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include "graph.h" + +#include + +#define MARGIN 10.0 + +Graph::Graph(QWidget *parent) + : QGraphicsView(parent) +{ + _scene = new QGraphicsScene(this); + setScene(_scene); + + _xAxis = new AxisItem(AxisItem::X); + _yAxis = new AxisItem(AxisItem::Y); + + _slider = new SliderItem(); + connect(_slider, SIGNAL(positionChanged(const QPointF&)), this, + SLOT(emitSliderPositionChanged(const QPointF&))); + + _xMax = -FLT_MAX; + _xMin = FLT_MAX; + _yMax = -FLT_MAX; + _yMin = FLT_MAX; + + _xScale = 1; + _yScale = 1; +} + +Graph::~Graph() +{ + if (_xAxis->scene() != _scene) + delete _xAxis; + if (_yAxis->scene() != _scene) + delete _yAxis; + + if (_slider->scene() != _scene) + delete _slider; + + delete _scene; +} + +void Graph::updateBounds(const QPointF &point) +{ + if (point.x() < _xMin) + _xMin = point.x(); + if (point.x() > _xMax) + _xMax = point.x(); + if (point.y() < _yMin) + _yMin = point.y(); + if (point.y() > _yMax) + _yMax = point.y(); +} + +void Graph::loadData(const QVector &data) +{ + QPainterPath path; + QGraphicsPathItem *pi; + QColor color = _colorShop.color(); + + + if (data.size() < 2) + return; + + updateBounds(data.at(0)); + path.moveTo(data.at(0).x(), -data.at(0).y()); + for (int i = 1; i < data.size(); i++) { + path.lineTo(data.at(i).x(), -data.at(i).y()); + updateBounds(data.at(i)); + } + + QBrush brush(color, Qt::SolidPattern); + QPen pen(brush, 0); + + pi = new QGraphicsPathItem(path); + pi->setPen(pen); + _scene->addItem(pi); + _graphs.append(pi); + + resize(viewport()->size() - QSizeF(MARGIN, MARGIN)); +} + +void Graph::resize(const QSizeF &size) +{ + QRectF r; + QSizeF mx, my; + QTransform transform; + qreal xs, ys; + + + if (_xAxis->scene() == _scene) + _scene->removeItem(_xAxis); + if (_yAxis->scene() == _scene) + _scene->removeItem(_yAxis); + _xAxis->setRange(QPointF(_xMin * _xScale, _xMax * _xScale)); + _yAxis->setRange(QPointF(_yMin * _yScale, _yMax * _yScale)); + + if (_slider->scene() == _scene) + _scene->removeItem(_slider); + + for (int i = 0; i < _graphs.size(); i++) + _graphs.at(i)->resetTransform(); + + mx = _xAxis->margin(); + my = _yAxis->margin(); + r = _scene->itemsBoundingRect(); + xs = (size.width() - (my.width() + mx.width())) / r.width(); + ys = (size.height() - (mx.height() + my.height())) / r.height(); + transform.scale(xs, ys); + + for (int i = 0; i < _graphs.size(); i++) + _graphs.at(i)->setTransform(transform); + + r = _scene->itemsBoundingRect(); + _xAxis->setSize(r.width()); + _yAxis->setSize(r.height()); + _xAxis->setPos(r.bottomLeft()); + _yAxis->setPos(r.bottomLeft()); + _scene->addItem(_xAxis); + _scene->addItem(_yAxis); + + _slider->setArea(r); + _slider->setPos(r.bottomLeft()); + _scene->addItem(_slider); + + _scene->setSceneRect(_scene->itemsBoundingRect()); +} + +void Graph::resizeEvent(QResizeEvent *) +{ + if (!_graphs.empty()) + resize(viewport()->size() - QSizeF(MARGIN, MARGIN)); +} + +void Graph::plot(QPainter *painter, const QRectF &target) +{ + qreal ratio = target.width() / target.height(); + QSizeF orig = _scene->sceneRect().size(); + QSizeF canvas = QSizeF(orig.height() * ratio, orig.height()); + + resize(canvas); + _slider->setVisible(false); + _scene->render(painter, target, QRectF(), Qt::KeepAspectRatioByExpanding); + _slider->setVisible(true); + resize(orig); +} + +void Graph::clear() +{ + if (_xAxis->scene() == _scene) + _scene->removeItem(_xAxis); + if (_yAxis->scene() == _scene) + _scene->removeItem(_yAxis); + + if (_slider->scene() == _scene) + _scene->removeItem(_slider); + + _scene->clear(); + _graphs.clear(); + _colorShop.reset(); + + _xMax = -FLT_MAX; + _xMin = FLT_MAX; + _yMax = -FLT_MAX; + _yMin = FLT_MAX; + + _scene->setSceneRect(0, 0, 0, 0); +} + +void Graph::emitSliderPositionChanged(const QPointF &pos) +{ + qreal val = pos.x() / _slider->area().width(); + emit sliderPositionChanged(val); +} + +qreal Graph::sliderPosition() const +{ + return _slider->pos().x() / _slider->area().width(); +} + +void Graph::setSliderPosition(qreal pos) +{ + _slider->setPos(pos * _slider->area().width(), 0); +} diff --git a/src/graph.h b/src/graph.h new file mode 100644 index 00000000..2b4d22f3 --- /dev/null +++ b/src/graph.h @@ -0,0 +1,56 @@ +#ifndef GRAPH_H +#define GRAPH_H + +#include +#include +#include +#include +#include "axisitem.h" +#include "slideritem.h" +#include "colorshop.h" + + +class Graph : public QGraphicsView +{ + Q_OBJECT + +public: + Graph(QWidget *parent = 0); + ~Graph(); + + void loadData(const QVector &data); + void setXLabel(const QString &label) {_xAxis->setLabel(label);} + void setYLabel(const QString &label) {_yAxis->setLabel(label);} + void setXScale(qreal scale) {_xScale = scale;} + void setYScale(qreal scale) {_yScale = scale;} + + void plot(QPainter *painter, const QRectF &target); + void clear(); + + qreal sliderPosition() const; + void setSliderPosition(qreal pos); + +signals: + void sliderPositionChanged(qreal); + +protected: + void resizeEvent(QResizeEvent *); + +private slots: + void emitSliderPositionChanged(const QPointF &pos); + +private: + void updateBounds(const QPointF &point); + void resize(const QSizeF &size); + + + QGraphicsScene *_scene; + AxisItem *_xAxis, *_yAxis; + SliderItem *_slider; + qreal _xMin, _xMax, _yMin, _yMax; + QList _graphs; + qreal _xScale, _yScale; + ColorShop _colorShop; +}; + +#endif // GRAPH_H diff --git a/src/gui.cpp b/src/gui.cpp new file mode 100644 index 00000000..9c97e4df --- /dev/null +++ b/src/gui.cpp @@ -0,0 +1,320 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gui.h" +#include "config.h" +#include "icons.h" +#include "keys.h" +#include "gpx.h" +#include "graph.h" +#include "track.h" + +#include + + +GUI::GUI() +{ + createActions(); + createMenus(); + createToolBars(); + createTrackView(); + createTrackGraphs(); + createStatusBar(); + + connect(_elevationGraph, SIGNAL(sliderPositionChanged(qreal)), _track, + SLOT(movePositionMarker(qreal))); + connect(_speedGraph, SIGNAL(sliderPositionChanged(qreal)), _track, + SLOT(movePositionMarker(qreal))); + + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget(_track); + layout->addWidget(_trackGraphs); + + QWidget *widget = new QWidget; + widget->setLayout(layout); + setCentralWidget(widget); + + setWindowTitle(APP_NAME); + setUnifiedTitleAndToolBarOnMac(true); + + _dirIndex = -1; + + resize(600, 800); +} + +void GUI::createActions() +{ + // Action Groups + _fileActionGroup = new QActionGroup(this); + _fileActionGroup->setExclusive(false); + _fileActionGroup->setEnabled(false); + + + // General actions + _exitAction = new QAction(tr("Exit"), this); + connect(_exitAction, SIGNAL(triggered()), this, SLOT(close())); + + _aboutAction = new QAction(QIcon(QPixmap(APP_ICON)), + tr("About GPXSee"), this); + connect(_aboutAction, SIGNAL(triggered()), this, SLOT(about())); + _aboutQtAction = new QAction(tr("About Qt"), this); + connect(_aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt())); + + // File related actions + _openFileAction = new QAction(QIcon(QPixmap(OPEN_FILE_ICON)), + tr("Open"), this); + connect(_openFileAction, SIGNAL(triggered()), this, SLOT(openFile())); + _saveFileAction = new QAction(QIcon(QPixmap(SAVE_FILE_ICON)), + tr("Save"), this); + _saveFileAction->setActionGroup(_fileActionGroup); + connect(_saveFileAction, SIGNAL(triggered()), this, SLOT(saveFile())); + _saveAsAction = new QAction(QIcon(QPixmap(SAVE_AS_ICON)), + tr("Save as"), this); + _saveAsAction->setActionGroup(_fileActionGroup); + connect(_saveAsAction, SIGNAL(triggered()), this, SLOT(saveAs())); + _closeFileAction = new QAction(QIcon(QPixmap(CLOSE_FILE_ICON)), + tr("Close"), this); + _closeFileAction->setActionGroup(_fileActionGroup); + connect(_closeFileAction, SIGNAL(triggered()), this, SLOT(closeFile())); + + // POI actions + _openPOIAction = new QAction(QIcon(QPixmap(OPEN_FILE_ICON)), + tr("Load file"), this); + connect(_openPOIAction, SIGNAL(triggered()), this, SLOT(openPOIFile())); + _showPOIAction = new QAction(QIcon(QPixmap(SHOW_POI_ICON)), + tr("Show"), this); + _showPOIAction->setCheckable(true); + connect(_showPOIAction, SIGNAL(triggered()), this, SLOT(showPOI())); +} + +void GUI::createMenus() +{ + _fileMenu = menuBar()->addMenu(tr("File")); + _fileMenu->addAction(_openFileAction); + _fileMenu->addAction(_saveFileAction); + _fileMenu->addAction(_saveAsAction); + _fileMenu->addAction(_closeFileAction); + _fileMenu->addAction(_exitAction); + + _poiMenu = menuBar()->addMenu(tr("POI")); + _poiMenu->addAction(_openPOIAction); + _poiMenu->addAction(_showPOIAction); + + _aboutMenu = menuBar()->addMenu(tr("Help")); + _aboutMenu->addAction(_aboutAction); + _aboutMenu->addAction(_aboutQtAction); +} + +void GUI::createToolBars() +{ + _fileToolBar = addToolBar(tr("File")); + _fileToolBar->addAction(_openFileAction); + _fileToolBar->addAction(_saveFileAction); + _fileToolBar->addAction(_closeFileAction); + + _poiToolBar = addToolBar(tr("POI")); + _poiToolBar->addAction(_showPOIAction); +} + +void GUI::createTrackView() +{ + _track = new Track(this); +} + +void GUI::createTrackGraphs() +{ + _elevationGraph = new Graph; + _elevationGraph->setXLabel(tr("Distance [km]")); + _elevationGraph->setYLabel(tr("Elevation [m.a.s.l.]")); + _elevationGraph->setXScale(0.001); + + _speedGraph = new Graph; + _speedGraph->setXLabel(tr("Distance [km]")); + _speedGraph->setYLabel(tr("Speed [km/h]")); + _speedGraph->setXScale(0.001); + _speedGraph->setYScale(3.6); + + _trackGraphs = new QTabWidget; + _trackGraphs->addTab(_elevationGraph, tr("Elevation")); + _trackGraphs->addTab(_speedGraph, tr("Speed")); + connect(_trackGraphs, SIGNAL(currentChanged(int)), this, + SLOT(graphChanged(int))); + + _trackGraphs->setFixedHeight(200); + _trackGraphs->setSizePolicy( + QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed)); +} + +void GUI::createStatusBar() +{ + _fileName = new QLabel(); + _zoom = new QLabel(); + _zoom->setAlignment(Qt::AlignHCenter); + + statusBar()->addPermanentWidget(_fileName, 9); + statusBar()->addPermanentWidget(_zoom, 1); + statusBar()->setSizeGripEnabled(false); +} + +void GUI::about() +{ + QMessageBox::about(this, tr("About GPXSee"), + QString("

") + QString(APP_NAME" "APP_VERSION) + + QString("

") + tr("GPX viewer and analyzer") + QString("

") + + QString("

") + tr("GPXSee is distributed under the terms of the " + "GNU General Public License version 3. For more info about GPXSee visit " + "the project homepage at ") + + QString(""APP_HOMEPAGE".

")); +} + +void GUI::openFile() +{ + QStringList files = QFileDialog::getOpenFileNames(this, tr("Open file")); + QStringList list = files; + QStringList::Iterator it = list.begin(); + + while(it != list.end()) { + openFile(*it); + ++it; + } + + if (!list.empty()) + setDir(list.back()); +} + +bool GUI::openFile(const QString &fileName) +{ + GPX gpx; + + if (!fileName.isEmpty()) { + if (gpx.loadFile(fileName)) { + _elevationGraph->loadData(gpx.elevationGraph()); + _speedGraph->loadData(gpx.speedGraph()); + _track->loadData(gpx.track()); + if (_showPOIAction->isChecked()) + _track->loadPOI(_poi); + + _fileActionGroup->setEnabled(true); + _fileName->setText(fileName); + + return true; + } else { + QMessageBox::critical(this, tr("Error"), fileName + QString("\n\n") + + tr("Error loading GPX file:\n%1").arg(gpx.errorString())); + } + } + + return false; +} + +void GUI::openPOIFile() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Open POI file")); + + if (!fileName.isEmpty()) { + if (!_poi.loadFile(fileName)) { + QMessageBox::critical(this, tr("Error"), + tr("Error loading POI file:\n%1").arg(_poi.errorString())); + } else { + _showPOIAction->setChecked(true); + _track->loadPOI(_poi); + } + } +} + +void GUI::saveAs() +{ + QString fileName = QFileDialog::getSaveFileName(this, "Export to PDF", + QString(), "*.pdf"); + + if (!fileName.isEmpty()) { + saveFile(fileName); + _saveFileName = fileName; + } +} + +void GUI::saveFile() +{ + if (_saveFileName.isEmpty()) + emit saveAs(); + else + saveFile(_saveFileName); +} + +void GUI::saveFile(const QString &fileName) +{ + QPrinter printer(QPrinter::HighResolution); + printer.setPageSize(QPrinter::A4); + printer.setOrientation(_track->orientation()); + printer.setOutputFormat(QPrinter::PdfFormat); + printer.setOutputFileName(fileName); + + QPainter p(&printer); + int margin = (printer.paperRect().height() - printer.pageRect().height()) + / 2; + _track->plot(&p, QRectF(0, 0, printer.width(), (0.80 * printer.height()) + - margin)); + _elevationGraph->plot(&p, QRectF(0, 0.80 * printer.height(), + printer.width(), printer.height() * 0.20)); + p.end(); +} + +void GUI::closeFile() +{ + _elevationGraph->clear(); + _speedGraph->clear(); + _track->clear(); + _fileName->clear(); + + _fileActionGroup->setEnabled(false); +} + +void GUI::showPOI() +{ + if (_showPOIAction->isChecked()) + _track->loadPOI(_poi); + else + _track->clearPOI(); +} + +void GUI::graphChanged(int index) +{ + if (_trackGraphs->widget(index) == _elevationGraph) + _elevationGraph->setSliderPosition(_speedGraph->sliderPosition()); + else if (_trackGraphs->widget(index) == _speedGraph) + _speedGraph->setSliderPosition(_elevationGraph->sliderPosition()); +} + + +void GUI::keyPressEvent(QKeyEvent *event) +{ + if (_dirIndex < 0 || _dirFiles.count() == 1) + return; + + if (event->key() == PREV_KEY) { + if (_dirIndex == 0) + return; + closeFile(); + openFile(_dirFiles.at(--_dirIndex).absoluteFilePath()); + } + if (event->key() == NEXT_KEY) { + if (_dirIndex == _dirFiles.size() - 1) + return; + closeFile(); + openFile(_dirFiles.at(++_dirIndex).absoluteFilePath()); + } +} + +void GUI::setDir(const QString &file) +{ + QDir dir = QFileInfo(file).absoluteDir(); + _dirFiles = dir.entryInfoList(QStringList("*.gpx"), QDir::Files); + _dirIndex = _dirFiles.empty() ? -1 : _dirFiles.indexOf(file); +} diff --git a/src/gui.h b/src/gui.h new file mode 100644 index 00000000..e682b42f --- /dev/null +++ b/src/gui.h @@ -0,0 +1,85 @@ +#ifndef GUI_H +#define GUI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "poi.h" + +class Graph; +class Track; + +class GUI : public QMainWindow +{ + Q_OBJECT + +public: + GUI(); + + bool openFile(const QString &fileName); + void setDir(const QString &file); + +private slots: + void about(); + void saveFile(); + void saveAs(); + void openFile(); + void closeFile(); + void openPOIFile(); + void showPOI(); + void graphChanged(int); + +private: + void createActions(); + void createMenus(); + void createToolBars(); + void createStatusBar(); + + void createTrackView(); + void createTrackGraphs(); + + void keyPressEvent(QKeyEvent * event); + + void saveFile(const QString &fileName); + + QMenu *_fileMenu; + QMenu *_aboutMenu; + QMenu *_poiMenu; + + QToolBar *_fileToolBar; + QToolBar *_poiToolBar; + QTabWidget *_trackGraphs; + QActionGroup *_fileActionGroup; + + QAction *_exitAction; + QAction *_aboutAction; + QAction *_aboutQtAction; + QAction *_saveFileAction; + QAction *_saveAsAction; + QAction *_openFileAction; + QAction *_closeFileAction; + QAction *_openPOIAction; + QAction *_showPOIAction; + + QLabel *_fileName; + QLabel *_zoom; + + Graph *_elevationGraph; + Graph *_speedGraph; + Track *_track; + + POI _poi; + + QFileInfoList _dirFiles; + int _dirIndex; + + QString _saveFileName; +}; + +#endif // GUI_H diff --git a/src/icons.h b/src/icons.h new file mode 100644 index 00000000..e1b78fbc --- /dev/null +++ b/src/icons.h @@ -0,0 +1,11 @@ +#ifndef ICONS_H +#define ICONS_H + +#define APP_ICON ":/icons/gpxsee.png" +#define OPEN_FILE_ICON ":/icons/document-open.png" +#define SAVE_FILE_ICON ":/icons/document-save.png" +#define SAVE_AS_ICON ":/icons/document-save-as.png" +#define CLOSE_FILE_ICON ":/icons/dialog-close.png" +#define SHOW_POI_ICON ":/icons/flag.png" + +#endif /* ICONS_H */ diff --git a/src/keys.h b/src/keys.h new file mode 100644 index 00000000..96b4de1f --- /dev/null +++ b/src/keys.h @@ -0,0 +1,7 @@ +#ifndef KEYS_H +#define KEYS_H + +#define NEXT_KEY Qt::Key_Space +#define PREV_KEY Qt::Key_Backspace + +#endif // KEYS_H diff --git a/src/ll.cpp b/src/ll.cpp new file mode 100644 index 00000000..d7ec539d --- /dev/null +++ b/src/ll.cpp @@ -0,0 +1,27 @@ +#include +#include "ll.h" + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif // M_PI + +#define WGS84_RADIUS 6378137.0 +#define deg2rad(d) (((d)*M_PI)/180.0) +#define rad2deg(d) (((d)*180.0)/M_PI) + +qreal llDistance(const QPointF &p1, const QPointF &p2) +{ + qreal dLat = deg2rad(p2.y() - p1.y()); + qreal dLon = deg2rad(p2.x() - p1.x()); + qreal a = pow(sin(dLat / 2.0), 2.0) + + cos(deg2rad(p1.y())) * cos(deg2rad(p2.y())) * pow(sin(dLon / 2.0), 2.0); + qreal c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a)); + + return (WGS84_RADIUS * c); +} + +void ll2mercator(const QPointF &src, QPointF &dst) +{ + dst.setX(src.x()); + dst.setY(rad2deg(log(tan(M_PI/4.0 + deg2rad(src.y())/2.0)))); +} diff --git a/src/ll.h b/src/ll.h new file mode 100644 index 00000000..92c90f02 --- /dev/null +++ b/src/ll.h @@ -0,0 +1,9 @@ +#ifndef LL_H +#define LL_H + +#include + +void ll2mercator(const QPointF &src, QPointF &dst); +qreal llDistance(const QPointF &p1, const QPointF &p2); + +#endif // LL_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 00000000..b4ac18f7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include "gui.h" +#include "icons.h" + + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QString locale = QLocale::system().name(); + QTranslator translator; + translator.load(QString(":/lang/gpxsee_") + locale); + app.installTranslator(&translator); + + GUI gui; + gui.setWindowIcon(QIcon(QPixmap(APP_ICON))); + gui.show(); + + for (int i = 1; i < argc; i++) + gui.openFile(argv[i]); + + if (argc > 1) + gui.setDir(QString(argv[argc - 1])); + + return app.exec(); +} diff --git a/src/markeritem.cpp b/src/markeritem.cpp new file mode 100644 index 00000000..7ba659e2 --- /dev/null +++ b/src/markeritem.cpp @@ -0,0 +1,27 @@ +#include +#include "markeritem.h" + +#define SIZE 8 + +MarkerItem::MarkerItem() +{ + +} + +QRectF MarkerItem::boundingRect() const +{ + return QRectF(-SIZE/2, -SIZE/2, SIZE, SIZE); +} + +void MarkerItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setPen(Qt::red); + painter->drawLine(-SIZE/2, 0, SIZE/2, 0); + painter->drawLine(0, -SIZE/2, 0, SIZE/2); + +// painter->drawRect(boundingRect()); +} diff --git a/src/markeritem.h b/src/markeritem.h new file mode 100644 index 00000000..ff80bda9 --- /dev/null +++ b/src/markeritem.h @@ -0,0 +1,16 @@ +#ifndef MARKERITEM_H +#define MARKERITEM_H + +#include + +class MarkerItem : public QGraphicsItem +{ +public: + MarkerItem(); + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget); +}; + +#endif // MARKERITEM_H diff --git a/src/parser.cpp b/src/parser.cpp new file mode 100644 index 00000000..ab09335d --- /dev/null +++ b/src/parser.cpp @@ -0,0 +1,93 @@ +#include "parser.h" + + +void Parser::handleTrekPointData(QVector &data, + QStringRef element, const QString &value) +{ + if (element == "ele") + data.last().elevation = value.toLatin1().toDouble(); + if (element == "time") + data.last().timestamp = QDateTime::fromString(value.toLatin1(), + Qt::ISODate); +} + +void Parser::handleTrekPointAttributes(QVector &data, + const QXmlStreamAttributes &attr) +{ + data.last().coordinates.setY(attr.value("lat").toLatin1().toDouble()); + data.last().coordinates.setX(attr.value("lon").toLatin1().toDouble()); +} + + +void Parser::trekPointData(QVector &data) +{ + while (_reader.readNextStartElement()) { + if (_reader.name() == "ele" || _reader.name() == "time") + handleTrekPointData(data, _reader.name(), _reader.readElementText()); + else + _reader.skipCurrentElement(); + } +} + +void Parser::trekPoints(QVector &data) +{ + QXmlStreamAttributes attr; + + while (_reader.readNextStartElement()) { + if (_reader.name() == "trkpt") { + attr = _reader.attributes(); + data.append(TrackPoint()); + handleTrekPointAttributes(data, attr); + trekPointData(data); + } else + _reader.skipCurrentElement(); + } +} + +void Parser::trek(QVector &data) +{ + while (_reader.readNextStartElement()) { + if (_reader.name() == "trkseg") { + trekPoints(data); + } else + _reader.skipCurrentElement(); + } +} + +void Parser::gpx(QVector &data) +{ + while (_reader.readNextStartElement()) { + if (_reader.name() == "trk") + trek(data); + else + _reader.skipCurrentElement(); + } +} + +bool Parser::parse(QVector &data) +{ + if (_reader.readNextStartElement()) { + if (_reader.name() == "gpx") + gpx(data); + else + _reader.raiseError(QObject::tr("Not a GPX file.")); + } + + return !_reader.error(); +} + + +QString Parser::errorString() const +{ + return QObject::tr("%1\nLine %2") + .arg(_reader.errorString()) + .arg(_reader.lineNumber()); +} + +bool Parser::loadFile(QIODevice *device, QVector &data) +{ + _reader.clear(); + _reader.setDevice(device); + + return parse(data); +} diff --git a/src/parser.h b/src/parser.h new file mode 100644 index 00000000..12dfb724 --- /dev/null +++ b/src/parser.h @@ -0,0 +1,36 @@ +#ifndef PARSER_H +#define PARSER_H + +#include +#include +#include + +struct TrackPoint +{ + QPointF coordinates; + QDateTime timestamp; + qreal elevation; +}; + +class Parser +{ +public: + bool loadFile(QIODevice *device, QVector &data); + QString errorString() const; + +private: + bool parse(QVector &data); + void gpx(QVector &data); + void trek(QVector &data); + void trekPoints(QVector &data); + void trekPointData(QVector &data); + + void handleTrekPointAttributes(QVector &data, + const QXmlStreamAttributes &attr); + void handleTrekPointData(QVector &data, QStringRef element, + const QString &value); + + QXmlStreamReader _reader; +}; + +#endif // PARSER_H diff --git a/src/poi.cpp b/src/poi.cpp new file mode 100644 index 00000000..2530420a --- /dev/null +++ b/src/poi.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include "ll.h" +#include "poi.h" + + +bool POI::loadFile(const QString &fileName) +{ + QFile file(fileName); + bool ret; + int ln = 1; + + _error.clear(); + + if (!file.open(QFile::ReadOnly | QFile::Text)) { + _error = qPrintable(file.errorString()); + return false; + } + + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QList list = line.split(','); + if (list.size() < 3) { + _error = QString("Parse error\nLine %1").arg(ln); + return false; + } + + qreal lat = list[0].trimmed().toDouble(&ret); + if (!ret) { + _error = QObject::tr("Invalid latitude\nLine %1").arg(ln); + return false; + } + qreal lon = list[1].trimmed().toDouble(&ret); + if (!ret) { + _error = QObject::tr("Invalid longitude\nLine %1").arg(ln); + return false; + } + QByteArray ba = list[2].trimmed(); + + QPointF p; + Entry entry; + ll2mercator(QPointF(lon, lat), p); + entry.description = QString::fromUtf8(ba.data(), ba.size()); + entry.coordinates = p; + + _data.append(entry); + ln++; + } + + for (int i = 0; i < _data.size(); ++i) { + qreal c[2]; + c[0] = _data.at(i).coordinates.x(); + c[1] = _data.at(i).coordinates.y(); + _tree.Insert(c, c, &_data.at(i)); + } + + return true; +} + +static bool cb(const Entry* data, void* context) +{ + QSet *set = (QSet*) context; + set->insert(data); + + return true; +} + +#define RECT 0.01 +QVector POI::points(const QVector &path) const +{ + QVector ret; + QSet set; + qreal min[2], max[2]; + + for (int i = 0; i < path.count(); i++) { + min[0] = path.at(i).x() - RECT; + min[1] = path.at(i).y() - RECT; + max[0] = path.at(i).x() + RECT; + max[1] = path.at(i).y() + RECT; + _tree.Search(min, max, cb, &set); + } + + QSet::const_iterator i = set.constBegin(); + while (i != set.constEnd()) { + ret.append(*(*i)); + ++i; + } + + return ret; +} + +void POI::clear() +{ + _tree.RemoveAll(); + _data.clear(); +} diff --git a/src/poi.h b/src/poi.h new file mode 100644 index 00000000..18edfa89 --- /dev/null +++ b/src/poi.h @@ -0,0 +1,43 @@ +#ifndef POI_H +#define POI_H + +#include +#include +#include +#include "rtree.h" + +class Entry +{ +public: + QPointF coordinates; + QString description; + + bool operator==(const Entry &other) const + {return this->description == other.description + && this->coordinates == other.coordinates;} +}; + +inline uint qHash(const Entry &key) +{ + return ::qHash(key.description); +} + + +class POI +{ +public: + bool loadFile(const QString &fileName); + QString errorString() const {return _error;} + QVector points(const QVector &path) const; + + void clear(); + +private: + typedef RTree POITree; + + POITree _tree; + QVector _data; + QString _error; +}; + +#endif // POI_H diff --git a/src/poiitem.cpp b/src/poiitem.cpp new file mode 100644 index 00000000..8f571747 --- /dev/null +++ b/src/poiitem.cpp @@ -0,0 +1,51 @@ +#include +#include "poiitem.h" + +#include + +#define FONT_FAMILY "Arial" +#define FONT_SIZE 12 +#define POINT_SIZE 8 + + +POIItem::POIItem(const QString &text) +{ + _text = text; + updateBoundingRect(); +} + +void POIItem::updateBoundingRect() +{ + QFont font; + font.setPixelSize(FONT_SIZE); + font.setFamily(FONT_FAMILY); + QFontMetrics fm(font); + QRect ts = fm.tightBoundingRect(_text); + + _boundingRect = QRectF(0, 0, ts.width() + POINT_SIZE, + ts.height() + fm.descent() + POINT_SIZE); +} + +void POIItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + QFont font; + font.setPixelSize(FONT_SIZE); + font.setFamily(FONT_FAMILY); + QFontMetrics fm(font); + QRect ts = fm.tightBoundingRect(_text); + + painter->setFont(font); + painter->drawText(POINT_SIZE - qMax(ts.x(), 0), POINT_SIZE + ts.height(), + _text); + painter->setBrush(Qt::SolidPattern); + painter->drawEllipse(0, 0, POINT_SIZE, POINT_SIZE); + +/* + painter->setPen(Qt::red); + painter->setBrush(Qt::NoBrush); + painter->drawRect(boundingRect()); +*/ +} diff --git a/src/poiitem.h b/src/poiitem.h new file mode 100644 index 00000000..48d7395d --- /dev/null +++ b/src/poiitem.h @@ -0,0 +1,22 @@ +#ifndef POIITEM_H +#define POIITEM_H + +#include + +class POIItem : public QGraphicsItem +{ +public: + POIItem(const QString &text); + + QRectF boundingRect() const {return _boundingRect;} + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget); + +private: + void updateBoundingRect(); + + QString _text; + QRectF _boundingRect; +}; + +#endif // POIITEM_H diff --git a/src/rtree.h b/src/rtree.h new file mode 100644 index 00000000..ffb6f039 --- /dev/null +++ b/src/rtree.h @@ -0,0 +1,1313 @@ +#ifndef RTREE_H +#define RTREE_H + +#include +#include +#include +#include + +#define ASSERT assert // RTree uses ASSERT( condition ) + +#define Max(a,b) \ + (((a) > (b)) ? (a) : (b)) +#define Min(a,b) \ + (((a) < (b)) ? (a) : (b)) + +// +// RTree.h +// + +#define RTREE_TEMPLATE template +#define RTREE_QUAL RTree + +#define RTREE_DONT_USE_MEMPOOLS // This version does not contain a fixed memory allocator, fill in lines with EXAMPLE to implement one. +#define RTREE_USE_SPHERICAL_VOLUME // Better split classification, may be slower on some systems + +// Fwd decl +class RTFileStream; // File I/O helper class, look below for implementation and notes. + + +/// \class RTree +/// Implementation of RTree, a multidimensional bounding rectangle tree. +/// Example usage: For a 3-dimensional tree use RTree myTree; +/// +/// This modified, templated C++ version by Greg Douglas at Auran (http://www.auran.com) +/// +/// DATATYPE Referenced data, should be int, void*, obj* etc. no larger than sizeof and simple type +/// ELEMTYPE Type of element such as int or float +/// NUMDIMS Number of dimensions such as 2 or 3 +/// ELEMTYPEREAL Type of element that allows fractional and large values such as float or double, for use in volume calcs +/// +/// NOTES: Inserting and removing data requires the knowledge of its constant Minimal Bounding Rectangle. +/// This version uses new/delete for nodes, I recommend using a fixed size allocator for efficiency. +/// Instead of using a callback function for returned results, I recommend and efficient pre-sized, grow-only memory +/// array similar to MFC CArray or STL Vector for returning search query result. +/// +template +class RTree +{ +protected: + + struct Node; // Fwd decl. Used by other internal structs and iterator + +public: + + // These constant must be declared after Branch and before Node struct + // Stuck up here for MSVC 6 compiler. NSVC .NET 2003 is much happier. + enum + { + MAXNODES = TMAXNODES, ///< Max elements in node + MINNODES = TMINNODES, ///< Min elements in node + }; + + +public: + + RTree(); + virtual ~RTree(); + + /// Insert entry + /// \param a_min Min of bounding rect + /// \param a_max Max of bounding rect + /// \param a_dataId Positive Id of data. Maybe zero, but negative numbers not allowed. + void Insert(const ELEMTYPE a_min[NUMDIMS], const ELEMTYPE a_max[NUMDIMS], const DATATYPE& a_dataId); + + /// Remove entry + /// \param a_min Min of bounding rect + /// \param a_max Max of bounding rect + /// \param a_dataId Positive Id of data. Maybe zero, but negative numbers not allowed. + void Remove(const ELEMTYPE a_min[NUMDIMS], const ELEMTYPE a_max[NUMDIMS], const DATATYPE& a_dataId); + + /// Find all within search rectangle + /// \param a_min Min of search bounding rect + /// \param a_max Max of search bounding rect + /// \param a_resultCallback Callback function to return result. Callback should return 'true' to continue searching + /// \param a_context User context to pass as parameter to a_resultCallback + /// \return Returns the number of entries found + int Search(const ELEMTYPE a_min[NUMDIMS], const ELEMTYPE a_max[NUMDIMS], bool a_resultCallback(DATATYPE a_data, void* a_context), void* a_context) const; + + /// Remove all entries from tree + void RemoveAll(); + + /// Count the data elements in this container. This is slow as no internal counter is maintained. + int Count(); + + + /// Iterator is not remove safe. + class Iterator + { + private: + + enum { MAX_STACK = 32 }; // Max stack size. Allows almost n^32 where n is number of branches in node + + struct StackElement + { + Node* m_node; + int m_branchIndex; + }; + + public: + + Iterator() { Init(); } + + ~Iterator() { } + + /// Is iterator invalid + bool IsNull() { return (m_tos <= 0); } + + /// Is iterator pointing to valid data + bool IsNotNull() { return (m_tos > 0); } + + /// Access the current data element. Caller must be sure iterator is not NULL first. + DATATYPE& operator*() + { + ASSERT(IsNotNull()); + StackElement& curTos = m_stack[m_tos - 1]; + return curTos.m_node->m_branch[curTos.m_branchIndex].m_data; + } + + /// Access the current data element. Caller must be sure iterator is not NULL first. + const DATATYPE& operator*() const + { + ASSERT(IsNotNull()); + StackElement& curTos = m_stack[m_tos - 1]; + return curTos.m_node->m_branch[curTos.m_branchIndex].m_data; + } + + /// Find the next data element + bool operator++() { return FindNextData(); } + + /// Get the bounds for this node + void GetBounds(ELEMTYPE a_min[NUMDIMS], ELEMTYPE a_max[NUMDIMS]) + { + ASSERT(IsNotNull()); + StackElement& curTos = m_stack[m_tos - 1]; + Branch& curBranch = curTos.m_node->m_branch[curTos.m_branchIndex]; + + for(int index = 0; index < NUMDIMS; ++index) + { + a_min[index] = curBranch.m_rect.m_min[index]; + a_max[index] = curBranch.m_rect.m_max[index]; + } + } + + private: + + /// Reset iterator + void Init() { m_tos = 0; } + + /// Find the next data element in the tree (For internal use only) + bool FindNextData() + { + for(;;) + { + if(m_tos <= 0) + { + return false; + } + StackElement curTos = Pop(); // Copy stack top cause it may change as we use it + + if(curTos.m_node->IsLeaf()) + { + // Keep walking through data while we can + if(curTos.m_branchIndex+1 < curTos.m_node->m_count) + { + // There is more data, just point to the next one + Push(curTos.m_node, curTos.m_branchIndex + 1); + return true; + } + // No more data, so it will fall back to previous level + } + else + { + if(curTos.m_branchIndex+1 < curTos.m_node->m_count) + { + // Push sibling on for future tree walk + // This is the 'fall back' node when we finish with the current level + Push(curTos.m_node, curTos.m_branchIndex + 1); + } + // Since cur node is not a leaf, push first of next level to get deeper into the tree + Node* nextLevelnode = curTos.m_node->m_branch[curTos.m_branchIndex].m_child; + Push(nextLevelnode, 0); + + // If we pushed on a new leaf, exit as the data is ready at TOS + if(nextLevelnode->IsLeaf()) + { + return true; + } + } + } + } + + /// Push node and branch onto iteration stack (For internal use only) + void Push(Node* a_node, int a_branchIndex) + { + m_stack[m_tos].m_node = a_node; + m_stack[m_tos].m_branchIndex = a_branchIndex; + ++m_tos; + ASSERT(m_tos <= MAX_STACK); + } + + /// Pop element off iteration stack (For internal use only) + StackElement& Pop() + { + ASSERT(m_tos > 0); + --m_tos; + return m_stack[m_tos]; + } + + StackElement m_stack[MAX_STACK]; ///< Stack as we are doing iteration instead of recursion + int m_tos; ///< Top Of Stack index + + friend RTree; // Allow hiding of non-public functions while allowing manipulation by logical owner + }; + + /// Get 'first' for iteration + void GetFirst(Iterator& a_it) + { + a_it.Init(); + Node* first = m_root; + while(first) + { + if(first->IsInternalNode() && first->m_count > 1) + { + a_it.Push(first, 1); // Descend sibling branch later + } + else if(first->IsLeaf()) + { + if(first->m_count) + { + a_it.Push(first, 0); + } + break; + } + first = first->m_branch[0].m_child; + } + } + + /// Get Next for iteration + void GetNext(Iterator& a_it) { ++a_it; } + + /// Is iterator NULL, or at end? + bool IsNull(Iterator& a_it) { return a_it.IsNull(); } + + /// Get object at iterator position + DATATYPE& GetAt(Iterator& a_it) { return *a_it; } + +protected: + + /// Minimal bounding rectangle (n-dimensional) + struct Rect + { + ELEMTYPE m_min[NUMDIMS]; ///< Min dimensions of bounding box + ELEMTYPE m_max[NUMDIMS]; ///< Max dimensions of bounding box + }; + + /// May be data or may be another subtree + /// The parents level determines this. + /// If the parents level is 0, then this is data + struct Branch + { + Rect m_rect; ///< Bounds + union + { + Node* m_child; ///< Child node + DATATYPE m_data; ///< Data Id or Ptr + }; + }; + + /// Node for each branch level + struct Node + { + bool IsInternalNode() { return (m_level > 0); } // Not a leaf, but a internal node + bool IsLeaf() { return (m_level == 0); } // A leaf, contains data + + int m_count; ///< Count + int m_level; ///< Leaf is zero, others positive + Branch m_branch[MAXNODES]; ///< Branch + }; + + /// A link list of nodes for reinsertion after a delete operation + struct ListNode + { + ListNode* m_next; ///< Next in list + Node* m_node; ///< Node + }; + + /// Variables for finding a split partition + struct PartitionVars + { + int m_partition[MAXNODES+1]; + int m_total; + int m_minFill; + int m_taken[MAXNODES+1]; + int m_count[2]; + Rect m_cover[2]; + ELEMTYPEREAL m_area[2]; + + Branch m_branchBuf[MAXNODES+1]; + int m_branchCount; + Rect m_coverSplit; + ELEMTYPEREAL m_coverSplitArea; + }; + + Node* AllocNode(); + void FreeNode(Node* a_node); + void InitNode(Node* a_node); + void InitRect(Rect* a_rect); + bool InsertRectRec(Rect* a_rect, const DATATYPE& a_id, Node* a_node, Node** a_newNode, int a_level); + bool InsertRect(Rect* a_rect, const DATATYPE& a_id, Node** a_root, int a_level); + Rect NodeCover(Node* a_node); + bool AddBranch(Branch* a_branch, Node* a_node, Node** a_newNode); + void DisconnectBranch(Node* a_node, int a_index); + int PickBranch(Rect* a_rect, Node* a_node); + Rect CombineRect(Rect* a_rectA, Rect* a_rectB); + void SplitNode(Node* a_node, Branch* a_branch, Node** a_newNode); + ELEMTYPEREAL RectSphericalVolume(Rect* a_rect); + ELEMTYPEREAL RectVolume(Rect* a_rect); + ELEMTYPEREAL CalcRectVolume(Rect* a_rect); + void GetBranches(Node* a_node, Branch* a_branch, PartitionVars* a_parVars); + void ChoosePartition(PartitionVars* a_parVars, int a_minFill); + void LoadNodes(Node* a_nodeA, Node* a_nodeB, PartitionVars* a_parVars); + void InitParVars(PartitionVars* a_parVars, int a_maxRects, int a_minFill); + void PickSeeds(PartitionVars* a_parVars); + void Classify(int a_index, int a_group, PartitionVars* a_parVars); + bool RemoveRect(Rect* a_rect, const DATATYPE& a_id, Node** a_root); + bool RemoveRectRec(Rect* a_rect, const DATATYPE& a_id, Node* a_node, ListNode** a_listNode); + ListNode* AllocListNode(); + void FreeListNode(ListNode* a_listNode); + bool Overlap(Rect* a_rectA, Rect* a_rectB) const; + void ReInsert(Node* a_node, ListNode** a_listNode); + bool Search(Node* a_node, Rect* a_rect, int& a_foundCount, bool a_resultCallback(DATATYPE a_data, void* a_context), void* a_context) const; + void RemoveAllRec(Node* a_node); + void Reset(); + void CountRec(Node* a_node, int& a_count); + + Node* m_root; ///< Root of tree + ELEMTYPEREAL m_unitSphereVolume; ///< Unit sphere constant for required number of dimensions +}; + + +RTREE_TEMPLATE +RTREE_QUAL::RTree() +{ + ASSERT(MAXNODES > MINNODES); + ASSERT(MINNODES > 0); + + + // We only support machine word size simple data type eg. integer index or object pointer. + // Since we are storing as union with non data branch + ASSERT(sizeof(DATATYPE) == sizeof(void*) || sizeof(DATATYPE) == sizeof(int)); + + // Precomputed volumes of the unit spheres for the first few dimensions + const float UNIT_SPHERE_VOLUMES[] = { + 0.000000f, 2.000000f, 3.141593f, // Dimension 0,1,2 + 4.188790f, 4.934802f, 5.263789f, // Dimension 3,4,5 + 5.167713f, 4.724766f, 4.058712f, // Dimension 6,7,8 + 3.298509f, 2.550164f, 1.884104f, // Dimension 9,10,11 + 1.335263f, 0.910629f, 0.599265f, // Dimension 12,13,14 + 0.381443f, 0.235331f, 0.140981f, // Dimension 15,16,17 + 0.082146f, 0.046622f, 0.025807f, // Dimension 18,19,20 + }; + + m_root = AllocNode(); + m_root->m_level = 0; + m_unitSphereVolume = (ELEMTYPEREAL)UNIT_SPHERE_VOLUMES[NUMDIMS]; +} + + +RTREE_TEMPLATE +RTREE_QUAL::~RTree() +{ + Reset(); // Free, or reset node memory +} + + +RTREE_TEMPLATE +void RTREE_QUAL::Insert(const ELEMTYPE a_min[NUMDIMS], const ELEMTYPE a_max[NUMDIMS], const DATATYPE& a_dataId) +{ + #ifdef _DEBUG + for(int index=0; indexIsInternalNode()) // not a leaf node + { + for(int index = 0; index < a_node->m_count; ++index) + { + CountRec(a_node->m_branch[index].m_child, a_count); + } + } + else // A leaf node + { + a_count += a_node->m_count; + } +} + + +RTREE_TEMPLATE +void RTREE_QUAL::RemoveAll() +{ + // Delete all existing nodes + Reset(); + + m_root = AllocNode(); + m_root->m_level = 0; +} + + +RTREE_TEMPLATE +void RTREE_QUAL::Reset() +{ + #ifdef RTREE_DONT_USE_MEMPOOLS + // Delete all existing nodes + RemoveAllRec(m_root); + #else // RTREE_DONT_USE_MEMPOOLS + // Just reset memory pools. We are not using complex types + // EXAMPLE + #endif // RTREE_DONT_USE_MEMPOOLS +} + + +RTREE_TEMPLATE +void RTREE_QUAL::RemoveAllRec(Node* a_node) +{ + ASSERT(a_node); + ASSERT(a_node->m_level >= 0); + + if(a_node->IsInternalNode()) // This is an internal node in the tree + { + for(int index=0; index < a_node->m_count; ++index) + { + RemoveAllRec(a_node->m_branch[index].m_child); + } + } + FreeNode(a_node); +} + + +RTREE_TEMPLATE +typename RTREE_QUAL::Node* RTREE_QUAL::AllocNode() +{ + Node* newNode; + #ifdef RTREE_DONT_USE_MEMPOOLS + newNode = new Node; + #else // RTREE_DONT_USE_MEMPOOLS + // EXAMPLE + #endif // RTREE_DONT_USE_MEMPOOLS + InitNode(newNode); + return newNode; +} + + +RTREE_TEMPLATE +void RTREE_QUAL::FreeNode(Node* a_node) +{ + ASSERT(a_node); + + #ifdef RTREE_DONT_USE_MEMPOOLS + delete a_node; + #else // RTREE_DONT_USE_MEMPOOLS + // EXAMPLE + #endif // RTREE_DONT_USE_MEMPOOLS +} + + +// Allocate space for a node in the list used in DeletRect to +// store Nodes that are too empty. +RTREE_TEMPLATE +typename RTREE_QUAL::ListNode* RTREE_QUAL::AllocListNode() +{ + #ifdef RTREE_DONT_USE_MEMPOOLS + return new ListNode; + #else // RTREE_DONT_USE_MEMPOOLS + // EXAMPLE + #endif // RTREE_DONT_USE_MEMPOOLS +} + + +RTREE_TEMPLATE +void RTREE_QUAL::FreeListNode(ListNode* a_listNode) +{ + #ifdef RTREE_DONT_USE_MEMPOOLS + delete a_listNode; + #else // RTREE_DONT_USE_MEMPOOLS + // EXAMPLE + #endif // RTREE_DONT_USE_MEMPOOLS +} + + +RTREE_TEMPLATE +void RTREE_QUAL::InitNode(Node* a_node) +{ + a_node->m_count = 0; + a_node->m_level = -1; +} + + +RTREE_TEMPLATE +void RTREE_QUAL::InitRect(Rect* a_rect) +{ + for(int index = 0; index < NUMDIMS; ++index) + { + a_rect->m_min[index] = (ELEMTYPE)0; + a_rect->m_max[index] = (ELEMTYPE)0; + } +} + + +// Inserts a new data rectangle into the index structure. +// Recursively descends tree, propagates splits back up. +// Returns 0 if node was not split. Old node updated. +// If node was split, returns 1 and sets the pointer pointed to by +// new_node to point to the new node. Old node updated to become one of two. +// The level argument specifies the number of steps up from the leaf +// level to insert; e.g. a data rectangle goes in at level = 0. +RTREE_TEMPLATE +bool RTREE_QUAL::InsertRectRec(Rect* a_rect, const DATATYPE& a_id, Node* a_node, Node** a_newNode, int a_level) +{ + ASSERT(a_rect && a_node && a_newNode); + ASSERT(a_level >= 0 && a_level <= a_node->m_level); + + int index; + Branch branch; + Node* otherNode; + + // Still above level for insertion, go down tree recursively + if(a_node->m_level > a_level) + { + index = PickBranch(a_rect, a_node); + if (!InsertRectRec(a_rect, a_id, a_node->m_branch[index].m_child, &otherNode, a_level)) + { + // Child was not split + a_node->m_branch[index].m_rect = CombineRect(a_rect, &(a_node->m_branch[index].m_rect)); + return false; + } + else // Child was split + { + a_node->m_branch[index].m_rect = NodeCover(a_node->m_branch[index].m_child); + branch.m_child = otherNode; + branch.m_rect = NodeCover(otherNode); + return AddBranch(&branch, a_node, a_newNode); + } + } + else if(a_node->m_level == a_level) // Have reached level for insertion. Add rect, split if necessary + { + branch.m_rect = *a_rect; + branch.m_child = (Node*) a_id; + // Child field of leaves contains id of data record + return AddBranch(&branch, a_node, a_newNode); + } + else + { + // Should never occur + ASSERT(0); + return false; + } +} + + +// Insert a data rectangle into an index structure. +// InsertRect provides for splitting the root; +// returns 1 if root was split, 0 if it was not. +// The level argument specifies the number of steps up from the leaf +// level to insert; e.g. a data rectangle goes in at level = 0. +// InsertRect2 does the recursion. +// +RTREE_TEMPLATE +bool RTREE_QUAL::InsertRect(Rect* a_rect, const DATATYPE& a_id, Node** a_root, int a_level) +{ + ASSERT(a_rect && a_root); + ASSERT(a_level >= 0 && a_level <= (*a_root)->m_level); + #ifdef _DEBUG + for(int index=0; index < NUMDIMS; ++index) + { + ASSERT(a_rect->m_min[index] <= a_rect->m_max[index]); + } + #endif //_DEBUG + + Node* newRoot; + Node* newNode; + Branch branch; + + if(InsertRectRec(a_rect, a_id, *a_root, &newNode, a_level)) // Root split + { + newRoot = AllocNode(); // Grow tree taller and new root + newRoot->m_level = (*a_root)->m_level + 1; + branch.m_rect = NodeCover(*a_root); + branch.m_child = *a_root; + AddBranch(&branch, newRoot, NULL); + branch.m_rect = NodeCover(newNode); + branch.m_child = newNode; + AddBranch(&branch, newRoot, NULL); + *a_root = newRoot; + return true; + } + + return false; +} + + +// Find the smallest rectangle that includes all rectangles in branches of a node. +RTREE_TEMPLATE +typename RTREE_QUAL::Rect RTREE_QUAL::NodeCover(Node* a_node) +{ + ASSERT(a_node); + + int firstTime = true; + Rect rect; + InitRect(&rect); + + for(int index = 0; index < a_node->m_count; ++index) + { + if(firstTime) + { + rect = a_node->m_branch[index].m_rect; + firstTime = false; + } + else + { + rect = CombineRect(&rect, &(a_node->m_branch[index].m_rect)); + } + } + + return rect; +} + + +// Add a branch to a node. Split the node if necessary. +// Returns 0 if node not split. Old node updated. +// Returns 1 if node split, sets *new_node to address of new node. +// Old node updated, becomes one of two. +RTREE_TEMPLATE +bool RTREE_QUAL::AddBranch(Branch* a_branch, Node* a_node, Node** a_newNode) +{ + ASSERT(a_branch); + ASSERT(a_node); + + if(a_node->m_count < MAXNODES) // Split won't be necessary + { + a_node->m_branch[a_node->m_count] = *a_branch; + ++a_node->m_count; + + return false; + } + else + { + ASSERT(a_newNode); + + SplitNode(a_node, a_branch, a_newNode); + return true; + } +} + + +// Disconnect a dependent node. +// Caller must return (or stop using iteration index) after this as count has changed +RTREE_TEMPLATE +void RTREE_QUAL::DisconnectBranch(Node* a_node, int a_index) +{ + ASSERT(a_node && (a_index >= 0) && (a_index < MAXNODES)); + ASSERT(a_node->m_count > 0); + + // Remove element by swapping with the last element to prevent gaps in array + a_node->m_branch[a_index] = a_node->m_branch[a_node->m_count - 1]; + + --a_node->m_count; +} + + +// Pick a branch. Pick the one that will need the smallest increase +// in area to accomodate the new rectangle. This will result in the +// least total area for the covering rectangles in the current node. +// In case of a tie, pick the one which was smaller before, to get +// the best resolution when searching. +RTREE_TEMPLATE +int RTREE_QUAL::PickBranch(Rect* a_rect, Node* a_node) +{ + ASSERT(a_rect && a_node); + + bool firstTime = true; + ELEMTYPEREAL increase; + ELEMTYPEREAL bestIncr = (ELEMTYPEREAL)-1; + ELEMTYPEREAL area; + ELEMTYPEREAL bestArea; + int best; + Rect tempRect; + + for(int index=0; index < a_node->m_count; ++index) + { + Rect* curRect = &a_node->m_branch[index].m_rect; + area = CalcRectVolume(curRect); + tempRect = CombineRect(a_rect, curRect); + increase = CalcRectVolume(&tempRect) - area; + if((increase < bestIncr) || firstTime) + { + best = index; + bestArea = area; + bestIncr = increase; + firstTime = false; + } + else if((increase == bestIncr) && (area < bestArea)) + { + best = index; + bestArea = area; + bestIncr = increase; + } + } + return best; +} + + +// Combine two rectangles into larger one containing both +RTREE_TEMPLATE +typename RTREE_QUAL::Rect RTREE_QUAL::CombineRect(Rect* a_rectA, Rect* a_rectB) +{ + ASSERT(a_rectA && a_rectB); + + Rect newRect; + + for(int index = 0; index < NUMDIMS; ++index) + { + newRect.m_min[index] = Min(a_rectA->m_min[index], a_rectB->m_min[index]); + newRect.m_max[index] = Max(a_rectA->m_max[index], a_rectB->m_max[index]); + } + + return newRect; +} + + + +// Split a node. +// Divides the nodes branches and the extra one between two nodes. +// Old node is one of the new ones, and one really new one is created. +// Tries more than one method for choosing a partition, uses best result. +RTREE_TEMPLATE +void RTREE_QUAL::SplitNode(Node* a_node, Branch* a_branch, Node** a_newNode) +{ + ASSERT(a_node); + ASSERT(a_branch); + + // Could just use local here, but member or external is faster since it is reused + PartitionVars localVars; + PartitionVars* parVars = &localVars; + int level; + + // Load all the branches into a buffer, initialize old node + level = a_node->m_level; + GetBranches(a_node, a_branch, parVars); + + // Find partition + ChoosePartition(parVars, MINNODES); + + // Put branches from buffer into 2 nodes according to chosen partition + *a_newNode = AllocNode(); + (*a_newNode)->m_level = a_node->m_level = level; + LoadNodes(a_node, *a_newNode, parVars); + + ASSERT((a_node->m_count + (*a_newNode)->m_count) == parVars->m_total); +} + + +// Calculate the n-dimensional volume of a rectangle +RTREE_TEMPLATE +ELEMTYPEREAL RTREE_QUAL::RectVolume(Rect* a_rect) +{ + ASSERT(a_rect); + + ELEMTYPEREAL volume = (ELEMTYPEREAL)1; + + for(int index=0; indexm_max[index] - a_rect->m_min[index]; + } + + ASSERT(volume >= (ELEMTYPEREAL)0); + + return volume; +} + + +// The exact volume of the bounding sphere for the given Rect +RTREE_TEMPLATE +ELEMTYPEREAL RTREE_QUAL::RectSphericalVolume(Rect* a_rect) +{ + ASSERT(a_rect); + + ELEMTYPEREAL sumOfSquares = (ELEMTYPEREAL)0; + ELEMTYPEREAL radius; + + for(int index=0; index < NUMDIMS; ++index) + { + ELEMTYPEREAL halfExtent = ((ELEMTYPEREAL)a_rect->m_max[index] - (ELEMTYPEREAL)a_rect->m_min[index]) * 0.5f; + sumOfSquares += halfExtent * halfExtent; + } + + radius = (ELEMTYPEREAL)sqrt(sumOfSquares); + + // Pow maybe slow, so test for common dims like 2,3 and just use x*x, x*x*x. + if(NUMDIMS == 3) + { + return (radius * radius * radius * m_unitSphereVolume); + } + else if(NUMDIMS == 2) + { + return (radius * radius * m_unitSphereVolume); + } + else + { + return (ELEMTYPEREAL)(pow(radius, NUMDIMS) * m_unitSphereVolume); + } +} + + +// Use one of the methods to calculate retangle volume +RTREE_TEMPLATE +ELEMTYPEREAL RTREE_QUAL::CalcRectVolume(Rect* a_rect) +{ + #ifdef RTREE_USE_SPHERICAL_VOLUME + return RectSphericalVolume(a_rect); // Slower but helps certain merge cases + #else // RTREE_USE_SPHERICAL_VOLUME + return RectVolume(a_rect); // Faster but can cause poor merges + #endif // RTREE_USE_SPHERICAL_VOLUME +} + + +// Load branch buffer with branches from full node plus the extra branch. +RTREE_TEMPLATE +void RTREE_QUAL::GetBranches(Node* a_node, Branch* a_branch, PartitionVars* a_parVars) +{ + ASSERT(a_node); + ASSERT(a_branch); + + ASSERT(a_node->m_count == MAXNODES); + + // Load the branch buffer + for(int index=0; index < MAXNODES; ++index) + { + a_parVars->m_branchBuf[index] = a_node->m_branch[index]; + } + a_parVars->m_branchBuf[MAXNODES] = *a_branch; + a_parVars->m_branchCount = MAXNODES + 1; + + // Calculate rect containing all in the set + a_parVars->m_coverSplit = a_parVars->m_branchBuf[0].m_rect; + for(int index=1; index < MAXNODES+1; ++index) + { + a_parVars->m_coverSplit = CombineRect(&a_parVars->m_coverSplit, &a_parVars->m_branchBuf[index].m_rect); + } + a_parVars->m_coverSplitArea = CalcRectVolume(&a_parVars->m_coverSplit); + + InitNode(a_node); +} + + +// Method #0 for choosing a partition: +// As the seeds for the two groups, pick the two rects that would waste the +// most area if covered by a single rectangle, i.e. evidently the worst pair +// to have in the same group. +// Of the remaining, one at a time is chosen to be put in one of the two groups. +// The one chosen is the one with the greatest difference in area expansion +// depending on which group - the rect most strongly attracted to one group +// and repelled from the other. +// If one group gets too full (more would force other group to violate min +// fill requirement) then other group gets the rest. +// These last are the ones that can go in either group most easily. +RTREE_TEMPLATE +void RTREE_QUAL::ChoosePartition(PartitionVars* a_parVars, int a_minFill) +{ + ASSERT(a_parVars); + + ELEMTYPEREAL biggestDiff; + int group, chosen, betterGroup; + + InitParVars(a_parVars, a_parVars->m_branchCount, a_minFill); + PickSeeds(a_parVars); + + while (((a_parVars->m_count[0] + a_parVars->m_count[1]) < a_parVars->m_total) + && (a_parVars->m_count[0] < (a_parVars->m_total - a_parVars->m_minFill)) + && (a_parVars->m_count[1] < (a_parVars->m_total - a_parVars->m_minFill))) + { + biggestDiff = (ELEMTYPEREAL) -1; + for(int index=0; indexm_total; ++index) + { + if(!a_parVars->m_taken[index]) + { + Rect* curRect = &a_parVars->m_branchBuf[index].m_rect; + Rect rect0 = CombineRect(curRect, &a_parVars->m_cover[0]); + Rect rect1 = CombineRect(curRect, &a_parVars->m_cover[1]); + ELEMTYPEREAL growth0 = CalcRectVolume(&rect0) - a_parVars->m_area[0]; + ELEMTYPEREAL growth1 = CalcRectVolume(&rect1) - a_parVars->m_area[1]; + ELEMTYPEREAL diff = growth1 - growth0; + if(diff >= 0) + { + group = 0; + } + else + { + group = 1; + diff = -diff; + } + + if(diff > biggestDiff) + { + biggestDiff = diff; + chosen = index; + betterGroup = group; + } + else if((diff == biggestDiff) && (a_parVars->m_count[group] < a_parVars->m_count[betterGroup])) + { + chosen = index; + betterGroup = group; + } + } + } + Classify(chosen, betterGroup, a_parVars); + } + + // If one group too full, put remaining rects in the other + if((a_parVars->m_count[0] + a_parVars->m_count[1]) < a_parVars->m_total) + { + if(a_parVars->m_count[0] >= a_parVars->m_total - a_parVars->m_minFill) + { + group = 1; + } + else + { + group = 0; + } + for(int index=0; indexm_total; ++index) + { + if(!a_parVars->m_taken[index]) + { + Classify(index, group, a_parVars); + } + } + } + + ASSERT((a_parVars->m_count[0] + a_parVars->m_count[1]) == a_parVars->m_total); + ASSERT((a_parVars->m_count[0] >= a_parVars->m_minFill) && + (a_parVars->m_count[1] >= a_parVars->m_minFill)); +} + + +// Copy branches from the buffer into two nodes according to the partition. +RTREE_TEMPLATE +void RTREE_QUAL::LoadNodes(Node* a_nodeA, Node* a_nodeB, PartitionVars* a_parVars) +{ + ASSERT(a_nodeA); + ASSERT(a_nodeB); + ASSERT(a_parVars); + + for(int index=0; index < a_parVars->m_total; ++index) + { + ASSERT(a_parVars->m_partition[index] == 0 || a_parVars->m_partition[index] == 1); + + if(a_parVars->m_partition[index] == 0) + { + AddBranch(&a_parVars->m_branchBuf[index], a_nodeA, NULL); + } + else if(a_parVars->m_partition[index] == 1) + { + AddBranch(&a_parVars->m_branchBuf[index], a_nodeB, NULL); + } + } +} + + +// Initialize a PartitionVars structure. +RTREE_TEMPLATE +void RTREE_QUAL::InitParVars(PartitionVars* a_parVars, int a_maxRects, int a_minFill) +{ + ASSERT(a_parVars); + + a_parVars->m_count[0] = a_parVars->m_count[1] = 0; + a_parVars->m_area[0] = a_parVars->m_area[1] = (ELEMTYPEREAL)0; + a_parVars->m_total = a_maxRects; + a_parVars->m_minFill = a_minFill; + for(int index=0; index < a_maxRects; ++index) + { + a_parVars->m_taken[index] = false; + a_parVars->m_partition[index] = -1; + } +} + + +RTREE_TEMPLATE +void RTREE_QUAL::PickSeeds(PartitionVars* a_parVars) +{ + int seed0, seed1; + ELEMTYPEREAL worst, waste; + ELEMTYPEREAL area[MAXNODES+1]; + + for(int index=0; indexm_total; ++index) + { + area[index] = CalcRectVolume(&a_parVars->m_branchBuf[index].m_rect); + } + + worst = -a_parVars->m_coverSplitArea - 1; + for(int indexA=0; indexA < a_parVars->m_total-1; ++indexA) + { + for(int indexB = indexA+1; indexB < a_parVars->m_total; ++indexB) + { + Rect oneRect = CombineRect(&a_parVars->m_branchBuf[indexA].m_rect, &a_parVars->m_branchBuf[indexB].m_rect); + waste = CalcRectVolume(&oneRect) - area[indexA] - area[indexB]; + if(waste > worst) + { + worst = waste; + seed0 = indexA; + seed1 = indexB; + } + } + } + Classify(seed0, 0, a_parVars); + Classify(seed1, 1, a_parVars); +} + + +// Put a branch in one of the groups. +RTREE_TEMPLATE +void RTREE_QUAL::Classify(int a_index, int a_group, PartitionVars* a_parVars) +{ + ASSERT(a_parVars); + ASSERT(!a_parVars->m_taken[a_index]); + + a_parVars->m_partition[a_index] = a_group; + a_parVars->m_taken[a_index] = true; + + if (a_parVars->m_count[a_group] == 0) + { + a_parVars->m_cover[a_group] = a_parVars->m_branchBuf[a_index].m_rect; + } + else + { + a_parVars->m_cover[a_group] = CombineRect(&a_parVars->m_branchBuf[a_index].m_rect, &a_parVars->m_cover[a_group]); + } + a_parVars->m_area[a_group] = CalcRectVolume(&a_parVars->m_cover[a_group]); + ++a_parVars->m_count[a_group]; +} + + +// Delete a data rectangle from an index structure. +// Pass in a pointer to a Rect, the tid of the record, ptr to ptr to root node. +// Returns 1 if record not found, 0 if success. +// RemoveRect provides for eliminating the root. +RTREE_TEMPLATE +bool RTREE_QUAL::RemoveRect(Rect* a_rect, const DATATYPE& a_id, Node** a_root) +{ + ASSERT(a_rect && a_root); + ASSERT(*a_root); + + Node* tempNode; + ListNode* reInsertList = NULL; + + if(!RemoveRectRec(a_rect, a_id, *a_root, &reInsertList)) + { + // Found and deleted a data item + // Reinsert any branches from eliminated nodes + while(reInsertList) + { + tempNode = reInsertList->m_node; + + for(int index = 0; index < tempNode->m_count; ++index) + { + InsertRect(&(tempNode->m_branch[index].m_rect), + tempNode->m_branch[index].m_data, + a_root, + tempNode->m_level); + } + + ListNode* remLNode = reInsertList; + reInsertList = reInsertList->m_next; + + FreeNode(remLNode->m_node); + FreeListNode(remLNode); + } + + // Check for redundant root (not leaf, 1 child) and eliminate + if((*a_root)->m_count == 1 && (*a_root)->IsInternalNode()) + { + tempNode = (*a_root)->m_branch[0].m_child; + + ASSERT(tempNode); + FreeNode(*a_root); + *a_root = tempNode; + } + return false; + } + else + { + return true; + } +} + + +// Delete a rectangle from non-root part of an index structure. +// Called by RemoveRect. Descends tree recursively, +// merges branches on the way back up. +// Returns 1 if record not found, 0 if success. +RTREE_TEMPLATE +bool RTREE_QUAL::RemoveRectRec(Rect* a_rect, const DATATYPE& a_id, Node* a_node, ListNode** a_listNode) +{ + ASSERT(a_rect && a_node && a_listNode); + ASSERT(a_node->m_level >= 0); + + if(a_node->IsInternalNode()) // not a leaf node + { + for(int index = 0; index < a_node->m_count; ++index) + { + if(Overlap(a_rect, &(a_node->m_branch[index].m_rect))) + { + if(!RemoveRectRec(a_rect, a_id, a_node->m_branch[index].m_child, a_listNode)) + { + if(a_node->m_branch[index].m_child->m_count >= MINNODES) + { + // child removed, just resize parent rect + a_node->m_branch[index].m_rect = NodeCover(a_node->m_branch[index].m_child); + } + else + { + // child removed, not enough entries in node, eliminate node + ReInsert(a_node->m_branch[index].m_child, a_listNode); + DisconnectBranch(a_node, index); // Must return after this call as count has changed + } + return false; + } + } + } + return true; + } + else // A leaf node + { + for(int index = 0; index < a_node->m_count; ++index) + { + if(a_node->m_branch[index].m_child == (Node*)a_id) + { + DisconnectBranch(a_node, index); // Must return after this call as count has changed + return false; + } + } + return true; + } +} + + +// Decide whether two rectangles overlap. +RTREE_TEMPLATE +bool RTREE_QUAL::Overlap(Rect* a_rectA, Rect* a_rectB) const +{ + ASSERT(a_rectA && a_rectB); + + for(int index=0; index < NUMDIMS; ++index) + { + if (a_rectA->m_min[index] > a_rectB->m_max[index] || + a_rectB->m_min[index] > a_rectA->m_max[index]) + { + return false; + } + } + return true; +} + + +// Add a node to the reinsertion list. All its branches will later +// be reinserted into the index structure. +RTREE_TEMPLATE +void RTREE_QUAL::ReInsert(Node* a_node, ListNode** a_listNode) +{ + ListNode* newListNode; + + newListNode = AllocListNode(); + newListNode->m_node = a_node; + newListNode->m_next = *a_listNode; + *a_listNode = newListNode; +} + + +// Search in an index tree or subtree for all data retangles that overlap the argument rectangle. +RTREE_TEMPLATE +bool RTREE_QUAL::Search(Node* a_node, Rect* a_rect, int& a_foundCount, bool (*a_resultCallback)(DATATYPE a_data, void* a_context), void* a_context) const +{ + ASSERT(a_node); + ASSERT(a_node->m_level >= 0); + ASSERT(a_rect); + + if(a_node->IsInternalNode()) // This is an internal node in the tree + { + for(int index=0; index < a_node->m_count; ++index) + { + if(Overlap(a_rect, &a_node->m_branch[index].m_rect)) + { + if(!Search(a_node->m_branch[index].m_child, a_rect, a_foundCount, a_resultCallback, a_context)) + { + return false; // Don't continue searching + } + } + } + } + else // This is a leaf node + { + for(int index=0; index < a_node->m_count; ++index) + { + if(Overlap(a_rect, &a_node->m_branch[index].m_rect)) + { + DATATYPE& id = a_node->m_branch[index].m_data; + + // NOTE: There are different ways to return results. Here's where to modify + if(a_resultCallback) + { + ++a_foundCount; + if(!a_resultCallback(id, a_context)) + { + return false; // Don't continue searching + } + } + } + } + } + + return true; // Continue searching +} + + +#undef RTREE_TEMPLATE +#undef RTREE_QUAL + +#endif //RTREE_H diff --git a/src/slideritem.cpp b/src/slideritem.cpp new file mode 100644 index 00000000..b1f7533e --- /dev/null +++ b/src/slideritem.cpp @@ -0,0 +1,49 @@ +#include +#include "slideritem.h" + +#include + +#define SIZE 10 + +SliderItem::SliderItem() +{ + setFlag(ItemIsMovable); + setFlag(ItemSendsGeometryChanges); +} + +QRectF SliderItem::boundingRect() const +{ + return QRectF(-SIZE/2, -_area.height(), SIZE, _area.height()); +} + +void SliderItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setPen(Qt::red); + painter->drawLine(0, 0, 0, -_area.height()); + + //painter->drawRect(boundingRect()); +} + +QVariant SliderItem::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionChange && scene()) { + QPointF pos = value.toPointF(); + + if (!_area.contains(QRectF(pos, boundingRect().size()))) { + pos.setX(qMin(_area.right(), qMax(pos.x(), _area.left()))); + pos.setY(qMin(_area.bottom(), qMax(pos.y(), _area.top() + + boundingRect().height()))); + + return pos; + } + } + + if (change == ItemPositionHasChanged) + emit positionChanged(value.toPointF()); + + return QGraphicsItem::itemChange(change, value); +} diff --git a/src/slideritem.h b/src/slideritem.h new file mode 100644 index 00000000..f415f9e8 --- /dev/null +++ b/src/slideritem.h @@ -0,0 +1,30 @@ +#ifndef SLIDERITEM_H +#define SLIDERITEM_H + +#include + +class SliderItem : public QGraphicsObject +{ + Q_OBJECT + +public: + SliderItem(); + + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, + QWidget *widget); + + QRectF area() {return _area;} + void setArea(const QRectF &area) {_area = area;} + +signals: + void positionChanged(const QPointF&); + +protected: + QVariant itemChange(GraphicsItemChange change, const QVariant &value); + +private: + QRectF _area; +}; + +#endif // SLIDERITEM_H diff --git a/src/track.cpp b/src/track.cpp new file mode 100644 index 00000000..9aff7ad5 --- /dev/null +++ b/src/track.cpp @@ -0,0 +1,192 @@ +#include +#include +#include +#include +#include +#include +#include "poiitem.h" +#include "markeritem.h" +#include "track.h" + +#include + + +Track::Track(QWidget *parent) + : QGraphicsView(parent) +{ + _scene = new QGraphicsScene(this); + setScene(_scene); + setResizeAnchor(QGraphicsView::AnchorViewCenter); + + _maxLen = 0; +} + +Track::~Track() +{ + delete _scene; +} + +void Track::loadData(const QVector &track) +{ + QPainterPath path; + QGraphicsPathItem *pi; + MarkerItem *mi; + QColor color = _colorShop.color(); + + if (track.size() < 2) + return; + + _tracks.append(track); + + path.moveTo(track.at(0).x(), -track.at(0).y()); + for (int i = 1; i < track.size(); i++) + path.lineTo(track.at(i).x(), -track.at(i).y()); + + _maxLen = qMax(path.length(), _maxLen); + + for (int i = 0; i < _trackPaths.size(); i++) { + _trackPaths.at(i)->resetTransform(); + _scene->removeItem(_markers.at(i)); + } + + QBrush brush(color, Qt::SolidPattern); + QPen pen(brush, 0); + pi = new QGraphicsPathItem(path); + pi->setPen(pen); + _scene->addItem(pi); + _trackPaths.append(pi); + + QTransform t = transform(); + + mi = new MarkerItem(); + mi->setPos(pi->path().pointAtPercent(0)); + _markers.append(mi); + + + for (int i = 0; i < _trackPaths.size(); i++) { + _markers.at(i)->setTransform(t); + _scene->addItem(_markers.at(i)); + } + + _scene->setSceneRect(_scene->itemsBoundingRect()); + fitInView(_scene->sceneRect(), Qt::KeepAspectRatio); +} + +QTransform Track::transform() const +{ + QPointF scale(_scene->itemsBoundingRect().width() / viewport()->width(), + _scene->itemsBoundingRect().height() / viewport()->height()); + QTransform transform; + transform.scale(qMax(scale.x(), scale.y()), qMax(scale.x(), scale.y())); + + return transform; +} + +void Track::loadPOI(const POI &poi) +{ + QHash::const_iterator it,jt; + + for (int i = 0; i < _tracks.size(); i++) { + QVector p = poi.points(_tracks.at(i)); + + for (int i = 0; i < p.size(); i++) { + if (_pois.contains(p.at(i))) + continue; + + POIItem *pi = new POIItem(p.at(i).description); + pi->setPos(p.at(i).coordinates.x(), -p.at(i).coordinates.y()); + pi->setTransform(transform()); + pi->setZValue(1); + _scene->addItem(pi); + + _pois.insert(p.at(i), pi); + } + } + + for (it = _pois.constBegin(); it != _pois.constEnd(); it++) + it.value()->setTransform(transform()); + + for (it = _pois.constBegin(); it != _pois.constEnd(); it++) { + for (jt = _pois.constBegin(); jt != _pois.constEnd(); jt++) { + if (it != jt && it.value()->isVisible() && jt.value()->isVisible() + && it.value()->collidesWithItem(jt.value())) + jt.value()->hide(); + } + } + + _scene->setSceneRect(_scene->itemsBoundingRect()); + fitInView(_scene->sceneRect(), Qt::KeepAspectRatio); +} + +void Track::wheelEvent(QWheelEvent *event) +{ + float factor; + + factor = pow(2.0, -event->delta() / 400.0); + scale(factor, factor); +} + +void Track::plot(QPainter *painter, const QRectF &target) +{ + QRectF orig = sceneRect(); + QRectF adj; + qreal ratio, diff; + + if (target.width()/target.height() > orig.width()/orig.height()) { + ratio = target.width()/target.height(); + diff = qAbs((orig.height() * ratio) - orig.width()); + adj = orig.adjusted(-diff/2, 0, diff/2, 0); + } else { + ratio = target.height()/target.width(); + diff = fabs((orig.width() * ratio) - orig.height()); + adj = orig.adjusted(0, -diff/2, 0, diff/2); + } + + for (int i = 0; i < _markers.size(); i++) + _markers.at(i)->setVisible(false); + _scene->render(painter, target, adj, Qt::KeepAspectRatioByExpanding); + for (int i = 0; i < _markers.size(); i++) + _markers.at(i)->setVisible(true); +} + +enum QPrinter::Orientation Track::orientation() const +{ + return (sceneRect().width() > sceneRect().height()) + ? QPrinter::Landscape : QPrinter::Portrait; +} + +void Track::clearPOI() +{ + QHash::const_iterator it; + + for (it = _pois.constBegin(); it != _pois.constEnd(); it++) { + _scene->removeItem(it.value()); + delete it.value(); + } + + _pois.clear(); +} + +void Track::clear() +{ + _pois.clear(); + _tracks.clear(); + _trackPaths.clear(); + _markers.clear(); + _scene->clear(); + _colorShop.reset(); + + _maxLen = 0; + + _scene->setSceneRect(0, 0, 0, 0); +} + +void Track::movePositionMarker(qreal val) +{ + for (int i = 0; i < _trackPaths.size(); i++) { + qreal f = _maxLen / _trackPaths.at(i)->path().length(); + QPointF pos = _trackPaths.at(i)->path().pointAtPercent(qMin(val * f, + 1.0)); + _markers.at(i)->setPos(pos); + } +} diff --git a/src/track.h b/src/track.h new file mode 100644 index 00000000..f5e2178a --- /dev/null +++ b/src/track.h @@ -0,0 +1,50 @@ +#ifndef TRACK_H +#define TRACK_H + +#include +#include +#include +#include +#include +#include "poi.h" +#include "colorshop.h" + + +class POIItem; +class MarkerItem; + +class Track : public QGraphicsView +{ + Q_OBJECT + +public: + Track(QWidget *parent = 0); + ~Track(); + + void loadData(const QVector &track); + void loadPOI(const POI &poi); + + void clearPOI(); + void clear(); + + void plot(QPainter *painter, const QRectF &target); + enum QPrinter::Orientation orientation() const; + +public slots: + void movePositionMarker(qreal val); + +private: + QTransform transform() const; + void wheelEvent(QWheelEvent *event); + + QGraphicsScene *_scene; + QList > _tracks; + QList _trackPaths; + QList _markers; + QHash _pois; + + ColorShop _colorShop; + qreal _maxLen; +}; + +#endif // TRACK_H